Merge pull request #268 from hashicorp/develop

v0.1.0-beta.3
This commit is contained in:
Randall Morey authored and GitHub committed 2020-10-02 16:41:09 -04:00
commit 58d767397e
1122 files changed
+74733

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Let us know about a bug!
title: ''
labels: ''
assignees: ''
---
<!-- Please reserve GitHub issues for bug reports and feature requests.
For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/boundary), as they will get more visibility from experienced users than the issue tracker.
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose by contacting us at security@hashicorp.com. Our PGP key is available at [our security page](https://www.hashicorp.com/security/).
-->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run `boundary authenticate ...`
2. Run `boundary connect ...`
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: Ask a question
url: https://discuss.hashicorp.com/c/boundary
about: For increased visibility, please post questions on the discussion forum.
+23
View File
@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest something!
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Explain any additional use-cases**
If there are any use-cases that would help us understand the use/need/value please share them as they can help us decide on acceptance and prioritization.
**Additional context**
Add any other context or screenshots about the feature request here.
+23
View File
@@ -0,0 +1,23 @@
addons:
- addons/**/*
addons/api:
- addons/api/**/*
addons/auth:
- addons/auth/**/*
addons/styles:
- addons/rose/**/*
ui:
- ui/**/*
ui/admin:
- ui/core/**/*
ui/admin/app:
- ui/core/app/**/*
ui/admin/tests:
- ui/core/tests/**/*
+88
View File
@@ -0,0 +1,88 @@
on:
issues:
types: [opened, closed, deleted, reopened]
pull_request_target:
types: [opened, closed, reopened]
issue_comment: # Also triggers when commenting on a PR from the conversation view
types: [created]
workflow_dispatch:
name: Jira Sync
jobs:
sync:
runs-on: ubuntu-latest
name: Jira sync
steps:
- name: Check if community user
if: github.event.action == 'opened'
id: boundary-team-role
run: |
TEAM=boundary
ROLE="$(hub api orgs/hashicorp/teams/${TEAM}/memberships/${{ github.actor }} | jq -r '.role | select(.!=null)')"
if [[ -n ${ROLE} ]]; then
echo "Actor ${{ github.actor }} is a ${TEAM} team member, skipping ticket creation"
else
echo "Actor ${{ github.actor }} is not a ${TEAM} team member"
fi
echo "::set-output name=role::${ROLE}"
env:
GITHUB_TOKEN: ${{ secrets.JIRA_SYNC_GITHUB_TOKEN }}
- name: Login
uses: atlassian/gajira-login@v2.0.0
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
- name: Set ticket type
if: github.event.action == 'opened' && !steps.boundary-team-role.outputs.role
id: set-ticket-type
run: |
if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then
echo "::set-output name=type::PR"
else
echo "::set-output name=type::ISS"
fi
- name: Create ticket
if: github.event.action == 'opened' && !steps.boundary-team-role.outputs.role
uses: tomhjp/gh-action-jira-create@v0.1.0
with:
project: ICU
issuetype: "GH Issue"
summary: "${{ github.event.repository.name }} [${{ steps.set-ticket-type.outputs.type }} #${{ github.event.issue.number || github.event.pull_request.number }}]: ${{ github.event.issue.title || github.event.pull_request.title }}"
description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created from GitHub Action for ${{ github.event.issue.html_url || github.event.pull_request.html_url }}, from ${{ github.actor }}_"
# customfield_10089 is Issue Link custom field
# customfield_10091 is team custom field
extraFields: '{"customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"}'
- name: Search
if: github.event.action != 'opened'
id: search
uses: tomhjp/gh-action-jira-search@v0.2.1
with:
# cf[10089] is Issue Link custom field
jql: 'issuetype = "GH Issue" and cf[10089]="${{ github.event.issue.html_url || github.event.pull_request.html_url }}"'
- name: Sync comment
if: github.event.action == 'created' && steps.search.outputs.issue
uses: atlassian/gajira-comment@v2.0.1
with:
issue: ${{ steps.search.outputs.issue }}
comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}"
- name: Close ticket
if: (github.event.action == 'closed' || github.event.action == 'deleted') && steps.search.outputs.issue
uses: atlassian/gajira-transition@v2.0.1
with:
issue: ${{ steps.search.outputs.issue }}
transition: Done
- name: Reopen ticket
if: github.event.action == 'reopened' && steps.search.outputs.issue
uses: atlassian/gajira-transition@v2.0.1
with:
issue: ${{ steps.search.outputs.issue }}
transition: "To Do"
+11
View File
@@ -0,0 +1,11 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+71
View File
@@ -0,0 +1,71 @@
name: Validate Monorepo
on: [push]
jobs:
compliance:
name: License Compliance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
id: cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn install
if: steps.cache.outputs.cache-hit != 'true'
- run: yarn run compliance:licenses
audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
id: cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn install
if: steps.cache.outputs.cache-hit != 'true'
- run: yarn run compliance:audit
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
id: cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn install
if: steps.cache.outputs.cache-hit != 'true'
- run: yarn lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
id: cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn install
if: steps.cache.outputs.cache-hit != 'true'
- run: yarn test
+29
View File
@@ -0,0 +1,29 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
dist
tmp
# dependencies
node_modules
# misc
.env*
.pnp*
.sass-cache
.DS_Store
connect.lock
coverage
coverage_*
coverage-*
libpeerconnection.log
npm-debug.log*
testem.log
yarn-error.log
storybook-static
**/.storybook/*.html
# ember-try
.node_modules.ember-try
bower.json.ember-try
package.json.ember-try
+154
View File
@@ -0,0 +1,154 @@
# Boundary UI Monorepo
![Validate Monorepo](https://github.com/hashicorp/boundary-ui/workflows/Validate%20Monorepo/badge.svg)
This monorepo contains multiple projects, including UIs and addons, used by
Boundary.
This top-level repository provides limited common tasks, such as installation
and commit assistance. However, most tasks must be executed from within a
subproject, e.g. running or testing.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Yarn Commands](#yarn-commands)
- [Contributing](#contributing)
- [Branching](#branching)
- [Building ToC](#building-toc)
- [Building Core UI for Production](#building-core-ui-for-production)
- [Building with a Container](#building-with-a-container)
- [Committing](#committing)
- [License Checking](#license-checking)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Prerequisites
You will need the following things properly installed on your computer.
* [Git][git]
* [Node.js][node]
* [Yarn][yarn] installed globally
* [Google Chrome][chrome]
* [Firefox][firefox]
[git]: https://git-scm.com/
[node]: https://nodejs.org/
[yarn]: https://classic.yarnpkg.com/lang/en/
[chrome]: https://google.com/chrome/
[firefox]: https://firefox.com/
[yarn-workspaces]: https://classic.yarnpkg.com/en/docs/workspaces/
## Installation
* `git clone <repository-url>` this repository
* `cd boundary-ui`
* `yarn install`
## Yarn Commands
List of available project commands. `yarn run <command-name>`
| Command | Description |
|---------------------|---|
| build | Builds Core UI. |
| test | Runs tests in CI mode. |
| commit | Replaces `git commit`, ensures compliance, audit, lint, and test checks pass before commit is allowed and normalizes commit messages across authors. |
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
| compliance:licenses | Checks that all dependencies have OSS-compatible licenses. |
| doc:toc | Automatically generates a table of contents for the README file. |
## Contributing
### Branching
We follow [A Successful Git Branching Model][nvie-git], otherwise known as
Gitflow. Branches `main` and `develop` are sacred mainlines into which we do
not commit directly. Instead, these branches have special meaning defining what
code may be found within. The `develop` branch contains code _for the next
release_. Code in `main` is _released, tagged, and ready for
production deployment_.
To start contributing:
1. Start a new **topic branch** off `develop`.
2. Commit work.
3. When work is complete, create a PR from the topic branch into `develop`.
4. Code review and automated checks occur. If checks fail or changes are
requested, commit more work. Rinse and repeat until all checks pass and no
changes are requested.
5. A core team member may merge the PR into `develop`. At this point, the code
is unreleased but considered "releasable".
6. **Releasing**: when it's time to release, create a PR from `develop` into
`main` (we skip Gitflow's `release` branch). This is tantamount to
requesting a new release (version) of the product. Ensure the body of the PR
includes well-documented changes slated for release.
7. Code review and automated checks occur.
8. A core team member may merge the PR into `main` to initiate a release.
9. Tag the merge commit in `main` with version number of the release and cut
a release in GitHub.
While Gitflow may seem heavy, most of its process can be automated. Gitflow is
similar to its lighter-weight sibling [GitHub Flow][github-flow]. In essence,
Gitflow differs only by adding an second mainline branch (`main`) that is
designated as _always containing production code_. Another happy characteristic
of Gitflow is that it's easy to track the project's release history, since
_every commit to `main` is a release by definition_. The well-defined nature
of releases assists in cross-team collaboration, ensuring everyone has
confidence about the state of the project.
### Building ToC
To autogenerate a ToC (table of contents) for this README,
run `yarn doc:toc`. Please update the ToC whenever editing the structure
of README.
### Building Core UI for Production
See ui/core/README.md Building for Production for more information on how to
build the Core UI. Be sure to set build-related environment variables as
necessary for your target environment, as described in the Core UI readme.
To quickly produce a production build of Core UI, run from this folder:
```bash
yarn install
yarn build
```
Assets are saved to `ui/core/dist/`.
#### Building with a Container
Optionally, you may build the UI within a container. Execute the following command:
```bash
docker-compose -f docker-compose-embedding.yml run build
```
Assets are saved to `ui/core/dist/`.
### Committing
Instead of the default `git commit`, we prescribe a custom command that ensures
linting and other checks pass before a commit may be completed. It also ensures
that all contributors author consistent commit messages. To get started with a
commit, stage your files as usual. Then run `yarn commit` and follow the
on-screen instructions to author your message.
#### License Checking
The licenses of dependencies are checking against a whitelist before every
commit. This helps catch undesirable licenses sooner (e.g. GPL). If your
commit fails due to an non-whitelisted license, you may add it to the whitelist
in `package.json` _as long as it is not a GPL variant or UNLICENSED_.
The change will be verified upon PR. GPL variants and UNLICENSED dependencies
will not be accepted.
[nvie-git]: https://nvie.com/posts/a-successful-git-branching-model/
[github-flow]: https://guides.github.com/introduction/flow/
+19
View File
@@ -0,0 +1,19 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.hbs]
insert_final_newline = false
[*.{diff,md}]
trim_trailing_whitespace = false
+9
View File
@@ -0,0 +1,9 @@
{
/**
Ember CLI sends analytics information by default. The data is completely
anonymous, but there are times when you might want to disable this behavior.
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": false
}
+20
View File
@@ -0,0 +1,20 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
+59
View File
@@ -0,0 +1,59 @@
'use strict';
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true
}
},
plugins: [
'ember'
],
extends: [
'eslint:recommended',
'plugin:ember/recommended'
],
env: {
browser: true
},
rules: {
'ember/no-jquery': 'error',
'ember/no-new-mixins': 0
},
overrides: [
// node files
{
files: [
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js',
'index.js',
'testem.js',
'blueprints/*/index.js',
'config/**/*.js',
'tests/dummy/config/**/*.js'
],
excludedFiles: [
'addon/**',
'addon-test-support/**',
'app/**',
'tests/dummy/app/**'
],
parserOptions: {
sourceType: 'script'
},
env: {
browser: false,
node: true
},
plugins: ['node'],
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
// add your custom rules and overrides for node files here
})
}
]
};
+32
View File
@@ -0,0 +1,32 @@
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
# misc
/.bowerrc
/.editorconfig
/.ember-cli
/.env*
/.eslintignore
/.eslintrc.js
/.git/
/.gitignore
/.template-lintrc.js
/.travis.yml
/.watchmanconfig
/bower.json
/config/ember-try.js
/CONTRIBUTING.md
/ember-cli-build.js
/testem.js
/tests/
/yarn.lock
.gitkeep
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"printWidth": 80
}
+5
View File
@@ -0,0 +1,5 @@
'use strict';
module.exports = {
extends: 'octane'
};
+60
View File
@@ -0,0 +1,60 @@
---
language: node_js
node_js:
# we recommend testing addons with the same minimum supported node version as Ember CLI
# so that your addon works for all apps
- "10"
dist: trusty
addons:
chrome: stable
cache:
directories:
- $HOME/.npm
env:
global:
# See https://git.io/vdao3 for details.
- JOBS=1
branches:
only:
- master
# npm version tags
- /^v\d+\.\d+\.\d+/
jobs:
fast_finish: true
allow_failures:
- env: EMBER_TRY_SCENARIO=ember-canary
include:
# runs linting and tests with current locked deps
- stage: "Tests"
name: "Tests"
script:
- npm run lint:hbs
- npm run lint:js
- npm test
- stage: "Additional Tests"
name: "Floating Dependencies"
install:
- npm install --no-package-lock
script:
- npm test
# we recommend new addons test the current and previous LTS
# as well as latest stable release (bonus points to beta/canary)
- env: EMBER_TRY_SCENARIO=ember-lts-3.12
- env: EMBER_TRY_SCENARIO=ember-lts-3.16
- env: EMBER_TRY_SCENARIO=ember-release
- env: EMBER_TRY_SCENARIO=ember-beta
- env: EMBER_TRY_SCENARIO=ember-canary
- env: EMBER_TRY_SCENARIO=ember-default-with-jquery
- env: EMBER_TRY_SCENARIO=ember-classic
script:
- node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO
+3
View File
@@ -0,0 +1,3 @@
{
"ignore_dirs": ["tmp", "dist"]
}
+81
View File
@@ -0,0 +1,81 @@
# API data layer Ember Addon for Boundary
This addon contains the API data access layer for Boundary.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Add API to an App](#add-api-to-an-app)
- [Installation](#installation)
- [Yarn Commands](#yarn-commands)
- [Linting](#linting)
- [Formatting](#formatting)
- [Running tests](#running-tests)
- [Running the dummy application](#running-the-dummy-application)
- [Contributing](#contributing)
- [Building ToC](#building-toc)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Add API to an App
Add this addon to an Ember application's `devDependencies` as:
`"api": "*"`, for applications included in this monorepo.
## Installation
See monorepo README for installation instructions.
## Yarn Commands
List of available project commands. `yarn run <command-name>`
| Command | Description |
| ------- | ----------- |
| build:development | Builds the dummy app in development mode. |
| build | Builds the dummy app for production. |
| lint | Runs all lint commands. |
| lint:js | Lints `js` files. |
| format | Runs all auto-formatters. |
| format:js | Auto-formats `js` files using Prettier. |
| start | Runs the dummy app local server. |
| test | Runs all tests. |
| test:all | Runs tests across multiple Ember versions with ember-try. |
| doc:toc | Automatically generates a table of contents for this README file. |
Additional commands in the monorepo package may affect this projects.
## Linting
* `yarn lint:hbs`
* `yarn lint:js`
* `yarn lint:js --fix`
## Formatting
Before submitting your work, be sure to run auto-formatters
(see commands above). This helps to ensure consistency among authors.
* `yarn format`
## Running tests
* `yarn test` – Runs the test suite on the current Ember version
* `yarn test --server` – Runs the test suite in "watch mode"
* `yarn test:all` – Runs the test suite against multiple Ember versions
## Running the dummy application
* `yarn start`
* Visit the dummy application at [http://localhost:4200](http://localhost:4200).
## Contributing
See monorepo README for more contribution instructions.
### Building ToC
To autogenerate a ToC (table of contents) for this README,
run `yarn doc:toc`. Please update the ToC whenever editing the structure
of README.
View File
Whitespace-only changes.
+274
View File
@@ -0,0 +1,274 @@
import RESTAdapter from '@ember-data/adapter/rest';
import { serializeIntoHash } from '@ember-data/adapter/-private';
import AdapterBuildURLMixin from '../mixins/adapter-build-url';
import config from 'ember-get-config';
import { get, getWithDefault } from '@ember/object';
import { InvalidError } from '@ember-data/adapter/error';
import { dasherize } from '@ember/string';
import { pluralize } from 'ember-inflector';
import { isArray } from '@ember/array';
/**
* Returns true if the payload is an empty object, false otherwise.
* Taken from Ember Data fetch-manager and "positivized" the name.
* @param {object} adapterPayload
* @return {boolean}
*/
function payloadIsBlank(adapterPayload) {
if (Array.isArray(adapterPayload)) {
return false;
} else {
return Object.keys(adapterPayload || {}).length === 0;
}
}
/**
* If the resonse is empty, returns a new payload containing an empty `items`
* array, as expected by the serializer. Otherwise, returns the response.
*
* Normally this would be performed 100% inside the serializer.
* However, the fetch manager, which we cannot easily override, glues the
* adapter and serializers together. Before it forwards a response from the
* adapter on to the serializer, it checks for an "empty" response. If the
* response is empty, fetch manager throws and error and never forwards it on.
* @param {object} response
* @return {object}
*/
function prenormalizeArrayResponse(response) {
return payloadIsBlank(response) ? { items: [] } : response;
}
export default class ApplicationAdapter extends RESTAdapter.extend(
AdapterBuildURLMixin
) {
// =attributes
/**
* Sets host to the `api.host` string in the application's config,
* if set.
* @type {string}
*/
host = get(config, 'api.host');
/**
* Sets namespace to the `api.namespace` string in the application's config,
* if set.
* @type {string}
*/
namespace = get(config, 'api.namespace');
// =methods
/**
* Appends a custom method after a colon, if a method is passed via the
* snapshot's `adapterOptions.method` field.
* @param {string} modelName
* @param {string} id
* @param {object} snapshot
* @return {string}
*/
urlSuffix(modelName, id, snapshot = {}) {
const method = getWithDefault(snapshot, 'adapterOptions.method', '');
return method ? `:${method}` : '';
}
/**
* Transforms the type to a dasherized string used in our API paths.
*
* @override
* @param {string} type
* @return {string}
*/
pathForType(type) {
return dasherize(pluralize(type));
}
/**
* Intercepts "empty" responses and adds an empty `items` array.
* @override
* @method findAll
* @return {Promise} promise
*/
findAll() {
return super.findAll(...arguments).then(prenormalizeArrayResponse);
}
/**
* Intercepts "empty" responses and adds an empty `items` array.
* @override
* @method query
* @return {Promise} promise
*/
query() {
return super.query(...arguments).then(prenormalizeArrayResponse);
}
/**
* Our API expects update calls with a custom method suffix
* (e.g. `:set-grants`) to be made with POST instead of PUT/PATCH.
* If this method is called with a custom method via `adapterOptions.method`,
* the API request will be made with POST. All other update requests are
* made as normal.
* @override
* @param {Store} store
* @param {Model} type
* @param {Snapshot} snapshot
* @return {Promise} promise
*/
updateRecord(store, type, snapshot) {
const method = get(snapshot, 'adapterOptions.method');
const data = serializeIntoHash(store, type, snapshot, {});
const id = snapshot.id;
const url = this.buildURL(type.modelName, id, snapshot, 'updateRecord');
return this.ajax(url, method ? 'POST' : 'PUT', { data });
}
/**
* Overrides default ajax method by rewritting PUT requests as PATCH.
* PATCH is the request method used by our API for updates.
*
* @override
* @method ajax
* @private
* @param {String} url
* @param {String} type The request type GET, POST, PUT, DELETE etc.
* @param {Object} options
* @return {Promise} promise
*/
ajax(url, type, options) {
const method = type === 'PUT' ? 'PATCH' : type;
return super.ajax(url, method, options);
}
/**
* Our API represents invalid requests via the 400 error. But Ember Data
* expects 422 by default. We change it here.
*
* @override
* @method isInvalid
* @param {Number} status
* @param {Object} headers
* @param {Object} payload
* @return {Boolean}
*/
isInvalid(status /*, headers, payload*/) {
return status === 400;
}
/**
* Ember Data blatantly assumes errors always present under an `errors` key
* in the payload, even _before normalizing errors_. Bizarre. Instead,
* we pass the payload through an adapter-level transformation step if the
* request was invalid. Then we can rely on the serializer's `extractErrors`
* to handle the rest.
*
* @override
* @method handleResponse
* @param {Number} status
* @param {Object} headers
* @param {Object} payload
* @param {Object} requestData - the original request information
* @return {Object | AdapterError} response
*/
handleResponse(status, headers, payload, requestData) {
if (this.isInvalid(status, headers, payload)) {
const detailedMessage = this.generatedDetailedMessage(...arguments);
const transformedPayload = this.transformValidationErrors(payload);
return new InvalidError(transformedPayload.errors, detailedMessage);
}
return super.handleResponse(status, headers, payload, requestData);
}
/**
* Add convenience booleans to each error within the errors list indicating
* the type of error that occurred. This is useful in downstream templates,
* e.g. `{{if error.isForbidden}}`.
*
* Error booleans are attached based on the error status code:
*
* - `401 isUnauthenticated`: the session isn't authenticated
* - `403 isForbidden`: the session is authenticated but does not have
* permission to perform the requested action
* - `404 isNotFound`: the requested resource could not be found
* - `500 isServer`: an internal server error occurred
* - `isUnknown`: an error occurred, but we don't know which or
* we don't distinguish it yet
*
* @override
* @method normalizeErrorResponse
* @param {Number} status
* @param {Object} headers
* @param {Object} payload
* @return {Array} errors payload
*/
normalizeErrorResponse(/*status, headers, payload*/) {
const errors = super.normalizeErrorResponse(...arguments);
if (isArray(errors)) {
errors.forEach((error) => {
switch (Number(error.status)) {
case 401:
error.isUnauthenticated = true;
break;
case 403:
error.isForbidden = true;
break;
case 404:
error.isNotFound = true;
break;
case 500:
error.isServer = true;
break;
default:
error.isUnknown = true;
}
});
}
return errors;
}
/**
* Normalizes the errors found in a payload from our API into JSON API
* error format.
*
* @param {object} payload
* @return {object}
*/
transformValidationErrors(payload) {
const fieldErrors = get(payload, 'details.request_fields') || [];
// Normalize the primary error message into "base", which is an error
// that applies to the whole model instance
const baseError = {
isInvalid: true,
status: payload.source,
code: payload.code,
detail: payload.message,
source: { pointer: '/data' },
};
// Normalize field-specific errors, if any.
const errors = fieldErrors.map((error) => ({
detail: error.description,
source: { pointer: `/data/attributes/${error.name}` },
}));
// Return a list of JSON API errors rooted under the `errors` key.
return {
errors: [baseError, ...errors],
};
}
/**
* Returns `payload.message`, which our API provides as a top-level
* description of the error that occurred.
*
* @override
* @method generatedDetailedMessage
* @private
* @param {Number} status
* @param {Object} headers
* @param {Object} payload
* @param {Object} requestData
* @return {String} detailed error message
*/
generatedDetailedMessage(status, headers, payload /*, requestData*/) {
return payload.message;
}
}
@@ -0,0 +1,46 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Account contains all fields related to an account resource
*/
export default class GeneratedAccountModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'The owning auth method ID.',
})
auth_method_id;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,46 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
*
*/
export default class GeneratedAuthMethodModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the resource is disabled',
})
disabled;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,8 @@
import BaseModel from '../../models/base';
/**
* NOTE: we don't yet use this model for display in the UI and thus no
* attributes are included at this time. This model is used only for
* validating that an auth token exists.
*/
export default class GeneratedAuthTokenModel extends BaseModel {}
@@ -0,0 +1,41 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Group contains all fields related to a Group resource
*/
export default class GeneratedGroupModel extends BaseModel {
// = attributes
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the resource is disabled',
})
disabled;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,51 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* HostCatalog manages Hosts and HostSets
*/
export default class GeneratedHostCatalogModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the catalog is disabled',
})
disabled;
@attr({
description: 'Attributes specific to the catalog type',
})
attributes;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,53 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* HostSet is a collection of Hosts created and managed by a HostCatalog
*/
export default class GeneratedHostSetModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
readOnly: true,
})
type;
@attr('string', {
description: 'The owning host catalog ID.',
})
host_catalog_id;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
@attr('number', {
description: 'The total count of hosts in this host set\nOutput only.',
readOnly: true,
})
size;
}
+46
View File
@@ -0,0 +1,46 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Host contains all fields related to a Host resource
*/
export default class GeneratedHostModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'The owning host catalog ID.',
})
host_catalog_id;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
+41
View File
@@ -0,0 +1,41 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Role contains all fields related to a Role resource
*/
export default class GeneratedRoleModel extends BaseModel {
// = attributes
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the resource is disabled',
})
disabled;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,41 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
*
*/
export default class GeneratedScopeModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource',
})
type;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,36 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
export default class GeneratedSessionModel extends BaseModel {
// =attributes
@attr('string') user_id;
@attr('string') target_id;
@attr('string') host_id;
@attr('string') host_set_id;
@attr('string', {
description: 'The name of the current status of this session.',
readOnly: true,
})
status;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,58 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Target
*/
export default class GeneratedTargetModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('number', {
description: '',
defaultValue: 28800
})
session_max_seconds;
@attr('number', {
description: '',
defaultValue: 1
})
session_connection_limit;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the catalog is disabled',
})
disabled;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
+41
View File
@@ -0,0 +1,41 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* User contains all fields related to a user resource
*/
export default class GeneratedUserModel extends BaseModel {
// =attributes
@attr('string', {
description: 'Optional name for identification purposes',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('boolean', {
description: 'Whether the resource is disabled',
})
disabled;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
@@ -0,0 +1,165 @@
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
/**
* Overrides URL-building methods in an adpater for greater flexibility.
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
* `snapshot`, which aren't normally passed. And adds support for a
* `urlSuffix` method, the return value of which is appended to the end of
* a generated URL.
*
* While this mixin provides an enhanced API, it doesn't actually change the
* way URLs are generated by default. Override methods in the adapter that
* uses this mixin to alter generated URLs.
*/
export default Mixin.create({
// =methods
/**
* This override behaves identically to the default method by default.
* It exists only to document the additional paramters to the method.
* @override
* @param {string} path
* @param {string} parentURL
* @param {string} modelName
* @param {string} id
* @param {object} snapshot
* @return {string} urlPrefix
*/
urlPrefix(/* path, parentURL, modelName, id, snapshot */) {
// What's this super syntax?
// Ember mixins are still based on the older Ember Object, rather than
// native ES classes. Thus we have to call super in this strange way.
// https://blog.emberjs.com/2019/01/26/emberjs-native-class-update-2019-edition.html
return this._super(...arguments);
},
/**
* Generates a string to be appended to a URL. Empty string by default.
* @param {string} modelName
* @param {string} id
* @param {?object} snapshot
* @return {string}
*/
urlSuffix(/* modelName, id, snapshot */) {
return '';
},
/**
* Builds the (mostly full) URL with a prefix, but not the suffix.
* The `_buildURL` method adds a suffix.
* Do not call directly.
* @override
* @param {String} modelName
* @param {String} id
* @return {String} url
*/
_buildPrefixedURL(modelName, id, snapshot) {
let path;
let url = [];
const host = get(this, 'host');
const prefix = this.urlPrefix(null, null, modelName, id, snapshot);
if (modelName) {
path = this.pathForType(modelName);
if (path) url.push(path);
}
if (id) url.push(encodeURIComponent(id));
if (prefix) url.unshift(prefix);
url = url.join('/');
if (!host && url && url.charAt(0) !== '/') url = '/' + url;
return `${url}`;
},
/**
* Overrides the default `_buildURL` with a nearly identical version.
* This method simply passes arguments into the `urlPrefix` method for
* convenience, where by default it receives no arguments.
*
* Adds a suffix, generated by the `urlSuffix` method, to the URL.
* @override
* @param {String} modelName
* @param {String} id
* @return {String} url
*/
_buildURL(modelName, id, snapshot) {
const url = this._buildPrefixedURL(modelName, id, snapshot);
const suffix = this.urlSuffix(modelName, id, snapshot);
return `${url}${suffix}`;
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} id
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForFindRecord(id, modelName, snapshot) {
return this._buildURL(modelName, id, snapshot);
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForFindAll(modelName, snapshot) {
return this._buildURL(modelName, null, snapshot);
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} id
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForFindBelongsTo(id, modelName, snapshot) {
return this._buildURL(modelName, id, snapshot);
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForCreateRecord(modelName, snapshot) {
return this._buildURL(modelName, null, snapshot);
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} id
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForUpdateRecord(id, modelName, snapshot) {
return this._buildURL(modelName, id, snapshot);
},
/**
* Overrides the default method simply to pass an additional argument
* `snapshot` to the `_buildURL` method.
* @override
* @param {string} id
* @param {string} modelName
* @param {object} snapshot
* @return {string}
*/
urlForDeleteRecord(id, modelName, snapshot) {
return this._buildURL(modelName, id, snapshot);
},
});
+81
View File
@@ -0,0 +1,81 @@
import GeneratedAccountModel from '../generated/models/account';
import { fragment } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
export default class AccountModel extends GeneratedAccountModel {
// =attributes
/**
* Attributes of this resource, if any, represented as a JSON fragment.
* @type {FragmentAccountAttributesModel}
*/
@fragment('fragment-account-attributes', { defaultValue: {} }) attributes;
/**
* Convenience for getting username in account.
* @type {string}
*/
@computed('type', 'attributes.{login_name}')
get username() {
let username = '';
switch (this.type) {
case 'password':
username = this.attributes.login_name;
break;
}
return username;
}
// =methods
/**
* Save account password via the `set-password` method.
* See serializer and adapter for more information.
* @param {string} password
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
setPassword(password, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'set-password',
password,
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Update account password via the `change-password` method.
* See serializer and adapter for more information.
* @param {string} currentPassword
* @param {string} newPassword
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
changePassword(
currentPassword,
newPassword,
options = { adapterOptions: {} }
) {
const defaultAdapterOptions = {
method: 'change-password',
currentPassword,
newPassword,
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
}
+3
View File
@@ -0,0 +1,3 @@
import GeneratedAuthMethodModel from '../generated/models/auth-method';
export default class AuthMethodModel extends GeneratedAuthMethodModel {}
+3
View File
@@ -0,0 +1,3 @@
import GeneratedAuthTokenModel from '../generated/models/auth-token';
export default class AuthTokenModel extends GeneratedAuthTokenModel {}
+86
View File
@@ -0,0 +1,86 @@
import Model from '@ember-data/model';
import { fragment } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
import { get } from '@ember/object';
/**
* Base model class provides common functionality for all models.
*
* One of the most interesting features of most resources is the
* `scope` field, which arrives from the API as an embedded JSON fragment.
* It contains information such as the parent scope ID and type. This scope ID
* is then used when generation scoped resource URLs via the
* `adapterOptions.scopeID` option (see `save` method below).
*
* For convenience, developers may interact with scope in a number of ways.
* The simplest method, when you don't care about other information, is to set
* the `scopeID`, e.g. `record.scopeID = 'o_123'`. The scope JSON fragment
* may be set directly, `record.scope = { scope_id: 'o_123' }`.
* For convenience, you may even set the scope using a `Scope` model:
* `record.scopeModel = scopeModelInstance`, which automatically converts the
* scope to a JSON fragment of the proper shape and assigns it to `scope`.
*
* A record's scope may be read in one of two ways:
*
* 1) As a JSON fragment: `record.scope`,
* 2) Or as an ID: `record.scopeID`.
*/
export default class BaseModel extends Model {
// =relationships
/**
* Scope of this resource, if any, represented as a JSON fragment.
* @type {FragmentScope}
*/
@fragment('fragment-scope') scope;
// =attributes
set scopeModel(model) {
if (model) {
const json = model.serialize();
json.scope_id = model.id;
this.scope = json;
}
}
/**
* Convenience for getting and setting the parent scope ID.
* @type {string
*/
@computed('scope.scope_id')
get scopeID() {
return get(this, 'scope.scope_id');
}
set scopeID(id) {
if (!this.scope) this.scope = {};
this.scope.scope_id = id;
}
/**
* Names are optional on models in our API. Thus we need to fallback on ID
* for display purposes.
* @type {string}
*/
@computed('name', 'id')
get displayName() {
return this.name || this.id;
}
/**
* @type {boolean}
*/
@computed('hasDirtyAttributes', 'isSaving')
get canSave() {
return this.hasDirtyAttributes && !this.isSaving;
}
/**
* @type {boolean}
*/
@computed('canSave')
get cannotSave() {
return !this.canSave;
}
}
@@ -0,0 +1,9 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentAccountAttributesModel extends Fragment {
@attr('string', {
description: 'The account login name',
})
login_name;
}
@@ -0,0 +1,9 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentHostAttributesModel extends Fragment {
@attr('string', {
description: 'The address (DNS or IP name) used to reach the host',
})
address;
}
@@ -0,0 +1,16 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentHostSetModel extends Fragment {
// =attributes
@attr('string', {
description: 'ID of the related host set',
})
host_set_id;
@attr('string', {
description: 'ID of the catalog to which the host set belongs',
})
host_catalog_id;
}
@@ -0,0 +1,21 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentPrincipal extends Fragment {
// =attributes
@attr('string', {
description: 'ID of the related scope',
})
scope_id;
@attr('string', {
description: 'ID of the related principal (user or group)',
})
principal_id;
@attr('string', {
description: 'Type of the related principal (user or group)',
})
type;
}
+42
View File
@@ -0,0 +1,42 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { computed } from '@ember/object';
import { attr } from '@ember-data/model';
import { scopeTypes } from './scope';
export default class FragmentScope extends Fragment {
// =attributes
@attr('string', {
description: 'ID of the related scope',
})
scope_id;
@attr('string', {
description: 'The type of the resource',
})
type;
/**
* @type {boolean}
*/
@computed('type')
get isGlobal() {
return this.type === scopeTypes.global;
}
/**
* @type {boolean}
*/
@computed('type')
get isOrg() {
return this.type === scopeTypes.org;
}
/**
* @type {boolean}
*/
@computed('type')
get isProject() {
return this.type === scopeTypes.project;
}
}
@@ -0,0 +1,14 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
/**
* Represents an item in an array of strings.
*/
export default class FragmentStringModel extends Fragment {
// =attributes
@attr('string', {
description: 'The value of the string.',
})
value;
}
@@ -0,0 +1,9 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentTargetAttributesModel extends Fragment {
@attr('number', {
description: 'The default port a target should use if present.',
})
default_port;
}
+94
View File
@@ -0,0 +1,94 @@
import GeneratedGroupModel from '../generated/models/group';
import { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { A } from '@ember/array';
export default class GroupModel extends GeneratedGroupModel {
// =attributes
/**
* Members is read-only under normal circumstances. But members can
* be persisted via calls to `addMembers()` or `removeMembers()`.
*/
@attr({
readOnly: true,
defaultValue: () => A(),
emptyArrayIfMissing: true,
})
member_ids;
/**
* An array of resolved user instances.
* Unresolvable instances are excluded from the array.
* @type {[UserModel]}
*/
// TODO: Use fragments array to read cross scope members.
@computed('member_ids.[]')
get members() {
return this.member_ids
.map((id) => this.store.peekRecord('user', id))
.filter(Boolean);
}
// =methods
/**
* Adds members via the `add-members` method.
* See serializer and adapter for more information.
* @param {[string]} memberIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addMembers(memberIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-members',
memberIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete members via the `remove-members` method.
* See serializer and adapter for more information.
* @param {[string]} memberIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeMembers(memberIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'remove-members',
memberIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete a single member via the `remove-members` method.
* @param {number} memberID
* @param {object} options
* @return {Promise}
*/
removeMember(memberID, options) {
return this.removeMembers([memberID], options);
}
}
+3
View File
@@ -0,0 +1,3 @@
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
export default class HostCatalogModel extends GeneratedHostCatalogModel {}
+73
View File
@@ -0,0 +1,73 @@
import GeneratedHostSetModel from '../generated/models/host-set';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
export default class HostSetModel extends GeneratedHostSetModel {
// =attributes
/**
* Grants is read-only _most_ under normal circumstances. But grants can
* be persisted via a dedicated call to `saveGrants()`.
*/
@fragmentArray('fragment-string', {
readOnly: true,
emptyArrayIfMissing: true,
})
host_ids;
// =methods
/**
* Adds hosts via the `add-hosts` method.
* See serializer and adapter for more information.
* @param {[string]} hostIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addHosts(hostIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-hosts',
hostIDs,
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete hosts via the `remove-hosts` method.
* See serializer and adapter for more information.
* @param {[string]} hostIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeHosts(hostIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'remove-hosts',
hostIDs,
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete a single host via the `remove-hosts` method.
* @param {string} hostID
* @param {object} options
* @return {Promise}
*/
removeHost(hostID, options) {
return this.removeHosts([hostID], options);
}
}
+12
View File
@@ -0,0 +1,12 @@
import GeneratedHostModel from '../generated/models/host';
import { fragment } from 'ember-data-model-fragments/attributes';
export default class HostModel extends GeneratedHostModel {
/**
* Attributes of this resource, if any, represented as a JSON fragment.
* @type {FragmentHostAttributesModel}
*/
@fragment('fragment-host-attributes', { defaultValue: {} }) attributes;
}
+101
View File
@@ -0,0 +1,101 @@
import GeneratedRoleModel from '../generated/models/role';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
export default class RoleModel extends GeneratedRoleModel {
// =attributes
/**
* Principals are users and groups assigned to a role. They are represented
* as references to User and Group instances. Since Ember Data relationships
* are wanting, we do not model these as a polymorphic relationship as might
* see obvious. Instead, the application layer is expected to load referenced
* users and groups as needed.
*/
@fragmentArray('fragment-principal', {
readOnly: true,
emptyArrayIfMissing: true,
})
principals;
/**
* Grants is read-only _most_ under normal circumstances. But grants can
* be persisted via a dedicated call to `saveGrants()`.
*/
@fragmentArray('fragment-string', { readOnly: true }) grants;
// =methods
/**
* Saves the `grants` array on the role via the `set-grants` method.
* See serializer and adapter for more information.
* @return {Promise}
*/
saveGrants() {
return this.save({
adapterOptions: {
method: 'set-grants',
serializeGrants: true,
},
});
}
/**
* Adds principals via the `add-principals` method.
* See serializer and adapter for more information.
* @param {[string]} principalIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addPrincipals(principalIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-principals',
principalIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete principals via the `remove-principals` method.
* See serializer and adapter for more information.
* @param {[string]} principalIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removePrincipals(principalIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'remove-principals',
principalIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete a single principal via the `remove-principals` method.
* @param {number} principalIDs
* @param {object} options
* @return {Promise}
*/
removePrincipal(principalID, options) {
return this.removePrincipals([principalID], options);
}
}
+44
View File
@@ -0,0 +1,44 @@
import GeneratedScopeModel from '../generated/models/scope';
import { computed } from '@ember/object';
export const scopeTypes = {
global: 'global',
org: 'org',
project: 'project',
};
export default class ScopeModel extends GeneratedScopeModel {
// =attributes
/**
* @type {boolean}
*/
@computed('type')
get isGlobal() {
return this.type === scopeTypes.global;
}
// There is only one global scope and it cannot be created by clients,
// thus no set.
/**
* @type {boolean}
*/
@computed('type')
get isOrg() {
return this.type === scopeTypes.org;
}
set isOrg(value) {
if (value) this.type = scopeTypes.org;
}
/**
* @type {boolean}
*/
@computed('type')
get isProject() {
return this.type === scopeTypes.project;
}
set isProject(value) {
if (value) this.type = scopeTypes.project;
}
}
+27
View File
@@ -0,0 +1,27 @@
import GeneratedSessionModel from '../generated/models/session';
export default class SessionModel extends GeneratedSessionModel {
// =methods
/**
* Cancels the session via the `cancel` method.
* See serializer and adapter for more information.
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
cancelSession(options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'cancel',
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
}
+100
View File
@@ -0,0 +1,100 @@
import GeneratedTargetModel from '../generated/models/target';
import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
export default class TargetModel extends GeneratedTargetModel {
// =attributes
/**
* Attributes of this resource, if any, represented as a JSON fragment.
* @type {FragmentTargetAttributesModel}
*/
@fragment('fragment-target-attributes', { defaultValue: {} }) attributes;
/**
* @type {[FragmentHostSetModel]}
*/
@fragmentArray('fragment-host-set', {
readOnly: true,
emptyArrayIfMissing: true,
})
host_sets;
/**
* An array of resolved host set and host catalog instances. Model instances
* must already be loaded into the store (this method will not load unloaded
* instances). Unresolvable instances are excluded from the array.
* @type {[{model: HostSetModel, hostCatalog: HostCatalogModel}]}
*/
@computed('host_sets.[]')
get hostSets() {
return this.host_sets
.map(({ host_set_id, host_catalog_id }) => ({
model: this.store.peekRecord('host-set', host_set_id),
hostCatalog: this.store.peekRecord('host-catalog', host_catalog_id),
}))
.filter((hostSetRef) => hostSetRef.model !== null);
}
// =methods
/**
* Adds host sets via the `add-host-sets` method.
* See serializer and adapter for more information.
* @param {[string]} hostSetIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addHostSets(hostSetIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-host-sets',
hostSetIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete host sets via the `remove-host-sets` method.
* See serializer and adapter for more information.
* @param {[string]} hostSetIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeHostSets(hostSetIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'remove-host-sets',
hostSetIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete a single host set via the `remove-host-sets` method.
* @param {number} hostSetID
* @param {object} options
* @return {Promise}
*/
removeHostSet(hostSetID, options) {
return this.removeHostSets([hostSetID], options);
}
}
+82
View File
@@ -0,0 +1,82 @@
import GeneratedUserModel from '../generated/models/user';
import { attr } from '@ember-data/model';
import { A } from '@ember/array';
export default class UserModel extends GeneratedUserModel {
// =attributes
/**
* Accounts is read-only under normal circumstances. But accounts can
* be persisted via calls to `addAccounts()` or `removeAccounts()`.
*/
@attr({
readOnly: true,
defaultValue: () => A(),
emptyArrayIfMissing: true,
})
account_ids;
// =methods
/**
* Adds accounts via the `add-accounts` method.
* See serializer and adapter for more information.
* @param {[string]} accountIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addAccounts(accountIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-accounts',
accountIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Remove accounts via the `remove-accounts` method.
* See serializer and adapter for more information.
* @param {[string]} accountIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeAccounts(accountIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'remove-accounts',
accountIDs,
};
// There is no "deep merge" in ES.
// All of this nonsense is here to ensure we get
// a decent merge of `adapterOptions`.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Remove a single account via the `remove-accounts` method.
* @param {number} accountID
* @param {object} options
* @return {Promise}
*/
removeAccount(accountID, options) {
return this.removeAccounts([accountID], options);
}
}
+69
View File
@@ -0,0 +1,69 @@
import ApplicationSerializer from './application';
export default class AccountSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
// =methods
/**
* If `adapterOptions.method` is `set-password`, the serialization should
* include only **password** and the version.
* If `adapterOptions.password` is set, the serialization should
* include **password** in it's `attributes`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
const password = snapshot?.adapterOptions?.password;
let serialized = super.serialize(...arguments);
if (password && snapshot?.record?.isNew)
serialized.attributes.password = password;
if (snapshot?.adapterOptions?.method === 'set-password') {
serialized = this.serializeForSetPassword(snapshot, password);
}
if (snapshot?.adapterOptions?.method === 'change-password') {
const { currentPassword, newPassword } = snapshot?.adapterOptions;
serialized = this.serializeForChangePassword(
snapshot,
currentPassword,
newPassword
);
}
return serialized;
}
/**
* Returns a payload containing only new password.
* @param {Snapshot} snapshot
* @param {string} password
* @return {object}
*/
serializeForSetPassword(snapshot, password) {
return {
version: snapshot.attr('version'),
password,
};
}
/**
* Returns a payload containing current and new passwords.
* @param {Snapshot} snapshot
* @param {string} current_password
* @param {string} new_password
* @return {object}
*/
serializeForChangePassword(snapshot, current_password, new_password) {
return {
version: snapshot.attr('version'),
current_password,
new_password,
};
}
}
+201
View File
@@ -0,0 +1,201 @@
import RESTSerializer from '@ember-data/serializer/rest';
import { underscore } from '@ember/string';
import { get } from '@ember/object';
import { copy } from 'ember-copy';
/**
* Manages serialization/normalization of data to/from the API.
*/
export default class ApplicationSerializer extends RESTSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = true;
// =methods
/**
* Generates an underscored key for the attribute.
* @param {string} attr
* @return {string}
*/
keyForAttribute(attr) {
return underscore(attr);
}
/**
* If an attribute is annotated as readOnly in the model, don't serialize it.
* Otherwise delegate to default attribute serializer.
*
* @override
* @method serializeAttribute
* @param {Snapshot} snapshot
* @param {Object} json
* @param {String} key
* @param {Object} attribute
*/
serializeAttribute(snapshot, json, key, { options }) {
let value = super.serializeAttribute(...arguments);
// Do not serialize read-only attributes.
if (options.readOnly) delete json[key];
// Version is sent only if it has a non-nullish value
if (key === 'version') {
if (json[key] === null || json[key] === undefined) delete json[key];
}
// Do not serialize `disabled` fields.
// TODO: disabled is temporarily disabled
if (key === 'disabled') delete json[key];
return value;
}
/**
* In our API, request payloads are unrooted. But Ember Data roots request
* payloads for this adapter by default. Instead of assigned a root key
* on the outgoing hash, we copy the serialized attributes into it.
*
* @override
* @method serializeIntoHash
* @param {Object} hash
* @param {Model} typeClass
* @param {Snapshot} snapshot
* @param {Object} options
*/
serializeIntoHash(hash, typeClass, snapshot, options) {
const serialized = this.serialize(snapshot, options);
Object.assign(hash, serialized);
}
/**
* Optionally serializes the scope ID into `scope_id`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
const serialized = super.serialize(...arguments);
// Delete `scope` field
delete serialized.scope;
// And serialize `scope_id`
if(this.serializeScopeID) {
const scope_id = snapshot?.attr('scope')?.attr('scope_id');
if (scope_id) serialized.scope_id = scope_id;
}
return serialized;
}
/**
* In our API, array payloads are always rooted under the same key `items`.
* Ember Data normally expects them to be rooted under their model name,
* e.g. `{hostCatalogs: […]}`. This method makes this transformation to
* accommodate Ember Data.
*
* @override
* @param {object} store
* @param {object} primaryModelClass
* @param {object} payload
* @param {?string} id
* @param {string} requestType
* @return {object}
*/
normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) {
// Setup a new payload data structure.
const transformedPayload = {};
// Find the Ember-data-expected root key name.
const payloadKey = this.payloadKeyFromModelName(
primaryModelClass.modelName
);
// Copy the data rooted under `items` in the existing payload into the new
// payload under the expected root key name.
transformedPayload[payloadKey] = copy(payload.items, true);
// Return the result of normalizing the transformed payload.
return super.normalizeArrayResponse(
store,
primaryModelClass,
transformedPayload,
id,
requestType
);
}
/**
* In our API, singluar resources are _unrooted_, whereas Ember Data expects
* them to be rooted under their model name, e.g. `{modelName: {…}}`.
* This method makes this transformation to accommodate Ember Data.
*
* @method normalizeSingleResponse
* @override
* @param {Store} store
* @param {Model} primaryModelClass
* @param {Object} payload
* @param {String|Number} id
* @param {String} requestType
* @return {Object} JSON-API Document
*/
normalizeSingleResponse(store, primaryModelClass, payload, id, requestType) {
// Copy payload (mostly to prevent mocking issues)
payload = copy(payload, true);
// Check for and normalize missing arrays
payload = this.normalizeMissingArrays(store, primaryModelClass, payload);
// Setup a new payload data structure.
const transformedPayload = {};
// Find the Ember-data-expected root key name.
const payloadKey = this.payloadKeyFromModelName(
primaryModelClass.modelName
);
// Copy the unrooted payload under the expected root key name.
transformedPayload[payloadKey] = payload;
// Return the result of normalizing the transformed payload.
return super.normalizeSingleResponse(
store,
primaryModelClass,
transformedPayload,
id,
requestType
);
}
/**
* Resets missing array fields to an empty array if they are annotated by
* `normalizeToEmptyArray: true` in the associated model attribute
* declaration. Our API excludes arrays when they are empty from
* singular responses.
*
* @param {Store} store
* @param {Model} primaryModelClass
* @param {object} payload
* @return {object}
*/
normalizeMissingArrays(store, primaryModelClass, payload) {
const attrDefs = store._attributesDefinitionFor(
primaryModelClass.modelName
);
if (attrDefs) {
Object.keys(attrDefs).forEach((key) => {
if (!payload[key] && attrDefs[key]?.options?.emptyArrayIfMissing) {
payload[key] = [];
}
});
}
return payload;
}
/**
* One consequence of using model fragments to represent the embedded scope
* is that they cannot have `id` fields. We still need to know scope IDs,
* so we copy the `scope.id` value into the `scope.scope_id` field.
* @override
* @see FragmentScope
* @param {Model} typeClass
* @param {Object} hash
* @return {Object}
*/
normalize(typeClass, hash) {
const normalizedHash = copy(hash, true);
const scopeID = get(normalizedHash, 'scope.id');
if (scopeID) normalizedHash.scope.scope_id = scopeID;
return super.normalize(typeClass, normalizedHash);
}
}
@@ -0,0 +1,16 @@
import JSONSerializer from '@ember-data/serializer/json';
export default class FragmentHostSetSerializer extends JSONSerializer {
// =methods
/**
* Renames `id` to `host_set_id` because fragments may not have IDs.
* Otherwise normalizes as normal.
* @param {ModelClass} typeClass
* @param {object} resourceHash
* @return {object}
*/
normalize(typeClass, { id: host_set_id, ...obj }) {
return super.normalize(typeClass, { host_set_id, ...obj });
}
}
@@ -0,0 +1,16 @@
import JSONSerializer from '@ember-data/serializer/json';
export default class FragmentPrincipalSerializer extends JSONSerializer {
// =methods
/**
* Renames `id` to `principal_id` because fragments may not have IDs.
* Otherwise normalizes as normal.
* @param {ModelClass} typeClass
* @param {object} resourceHash
* @return {object}
*/
normalize(typeClass, { id: principal_id, ...obj }) {
return super.normalize(typeClass, { principal_id, ...obj });
}
}
+32
View File
@@ -0,0 +1,32 @@
import ApplicationSerializer from './application';
export default class GroupSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.memberIDs` is set (to an array of user IDs),
* then the payload is serialized via `serializewithMembers`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
const memberIDs = snapshot?.adapterOptions?.memberIDs;
if (memberIDs) serialized = this.serializewithMembers(snapshot, memberIDs);
return serialized;
}
/**
* Returns a payload containing only the `member_ids` array (and version).
* @param {Snapshot} snapshot
* @param {[string]} memberIDs
* @return {object}
*/
serializewithMembers(snapshot, memberIDs) {
return {
version: snapshot.attr('version'),
member_ids: memberIDs,
};
}
}
+58
View File
@@ -0,0 +1,58 @@
import ApplicationSerializer from './application';
export default class HostSetSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
// =methods
/**
* If `adapterOptions.serializeHostIDs` is true, the serialization should
* include **only host_ids** and the version. Normally, these are not
* serialized.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
const hostIDs = snapshot?.adapterOptions?.hostIDs;
if (hostIDs) serialized = this.serializeWithHostIDs(snapshot, hostIDs);
return serialized;
}
/**
* Returns a payload containing only the host_ids array.
* @param {Snapshot} snapshot
* @param {[string]} hostIDs
* @return {object}
*/
serializeWithHostIDs(snapshot, hostIDs) {
return {
version: snapshot.attr('version'),
host_ids: hostIDs,
};
}
/**
* Converts string arrays to object arrays for use with `fragment-string`.
* E.g. `"foo"` -> `{value: "foo"}`
* @override
* @param {Model} modelClass
* @param {Object} resourceHash
* @return {Object}
*/
normalize(modelClass, resourceHash) {
// TODO: can fragment string normalization be handled generically?
// TODO: host_ids might actually be attributes.host_ids
if (resourceHash.host_ids) {
resourceHash.host_ids = resourceHash.host_ids.map((value) => ({ value }));
}
return super.normalize(modelClass, resourceHash);
}
}
+12
View File
@@ -0,0 +1,12 @@
import ApplicationSerializer from './application';
export default class HostSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
}
+72
View File
@@ -0,0 +1,72 @@
import ApplicationSerializer from './application';
import { get } from '@ember/object';
export default class RoleSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.serializeGrants` is true, the serialization should
* include **only grants** and the version. Normally, grants are not
* serialized.
* If `adapterOptions.principalIDs` is set (to an array of user and
* group IDs), then the payload is serialized via `serializewithPrincipals`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
const serializeGrants = get(snapshot, 'adapterOptions.serializeGrants');
let serialized = super.serialize(...arguments);
if (serializeGrants) serialized = this.serializeWithGrants(snapshot);
const principalIDs = snapshot?.adapterOptions?.principalIDs;
if (principalIDs)
serialized = this.serializewithPrincipals(snapshot, principalIDs);
return serialized;
}
/**
* Returns a payload containing only the grants array.
* @param {Snapshot} snapshot
* @return {object}
*/
serializeWithGrants(snapshot) {
return {
version: snapshot.attr('version'),
grant_strings: snapshot
.attr('grants')
.map((grant) => grant.attr('value')),
};
}
/**
* Returns a payload containing only the principal_ids array using IDs
* passed into the function (rather than existing principals on the model).
* @param {Snapshot} snapshot
* @param {[string]} principalIDs
* @return {object}
*/
serializewithPrincipals(snapshot, principalIDs) {
return {
version: snapshot.attr('version'),
principal_ids: principalIDs,
};
}
/**
* Converts string arrays to object arrays for use with `fragment-string`.
* E.g. `"foo"` -> `{value: "foo"}`
* @override
* @param {Model} typeClass
* @param {Object} hash
* @return {Object}
*/
normalize(modelClass, resourceHash) {
// TODO: can fragment string normalization be handled generically?
if (resourceHash.grant_strings) {
resourceHash.grants = resourceHash.grant_strings.map((value) => ({
value,
}));
}
return super.normalize(modelClass, resourceHash);
}
}
+31
View File
@@ -0,0 +1,31 @@
import ApplicationSerializer from './application';
export default class SessionSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.method` is `cancel`, the serialization should
* include the version field only.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
if (snapshot?.adapterOptions?.method === 'cancel') {
serialized = this.serializeForCancel(snapshot);
}
return serialized;
}
/**
* Returns a payload containing only version
* @param {Snapshot} snapshot
* @return {object}
*/
serializeForCancel(snapshot) {
return {
version: snapshot.attr('version'),
};
}
}
+35
View File
@@ -0,0 +1,35 @@
import ApplicationSerializer from './application';
export default class TargetSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.hostSets` is set to an array of host set models,
* the resulting target serialization should include **only host sets**
* and the version. Normally, host sets are not serialized.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
const hostSetIDs = snapshot?.adapterOptions?.hostSetIDs;
if (hostSetIDs)
serialized = this.serializeWithHostSets(snapshot, hostSetIDs);
return serialized;
}
/**
* Returns a payload containing only the host_set_ids array using IDs
* passed into the function (rather than existing host sets on the model).
* @param {Snapshot} snapshot
* @param {[string]} hostSetIDs
* @return {object}
*/
serializeWithHostSets(snapshot, hostSetIDs) {
return {
version: snapshot.attr('version'),
host_set_ids: hostSetIDs,
};
}
}
+33
View File
@@ -0,0 +1,33 @@
import ApplicationSerializer from './application';
export default class UserSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.accountIDs` is set (to an array of user IDs),
* then the payload is serialized via `serializewithAccounts`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
const accountIDs = snapshot?.adapterOptions?.accountIDs;
if (accountIDs) serialized =
this.serializewithAccounts(snapshot, accountIDs);
return serialized;
}
/**
* Returns a payload containing only the `account_ids` array (and version).
* @param {Snapshot} snapshot
* @param {[string]} account_ids
* @return {object}
*/
serializewithAccounts(snapshot, account_ids) {
return {
version: snapshot.attr('version'),
account_ids,
};
}
}
+130
View File
@@ -0,0 +1,130 @@
import Service from '@ember/service';
import { getOwner } from '@ember/application';
import { get } from '@ember/object';
/**
* A dead simple memory storage class used by `StorageService` when it
* is configued for memory-only.
*/
class MemoryStorage {
/**
* Internal storage object.
* @type {object}
* @private
*/
#storage = {};
/**
* Gets a value from storage.
* @param {string} key
* @return {any}
*/
getItem(key) {
return this.#storage[key];
}
/**
* Sets a value to storage.
* @param {string} key
*/
setItem(key, value) {
this.#storage[key] = value;
}
/**
* Removes a value from storage.
* @param {string} key
*/
removeItem(key) {
delete this.#storage[key];
}
}
/**
* The storage service provides a simple way to persist data in the browser.
* Most of the time localStorage is used. Appplications may specify in-memory
* storage via the config, for example:
*
* ENV.storage = {
* memory: true
* };
*
* @example
* import Route from '@ember/routing/route';
* import { inject as service } from '@ember/service';
* export default class ApplicationRoute extends Route {
* @service storage;
* afterModel() {
* this.storage.setItem('key', 'value');
* }
* }
*/
export default class StorageService extends Service {
/**
* @type {MemoryStorage}
* @private
*/
#memoryStorage = new MemoryStorage();
/**
* Gets the underlying storage mechanism.
* @return {Storage}
*/
get storage() {
// Get the application config to determine if memory storage is enabled
const config = getOwner(this).resolveRegistration('config:environment');
const inMemory = get(config, 'storage.memory');
// Get the global context object (window) via service lookup,
// because just reaching for a global is icky
const globalContext = getOwner(this).lookup('service:-document')
.documentElement.parentNode.defaultView;
// If memory storage enabled, return the memory storage instance,
// otherwise return localStorage.
return inMemory ? this.#memoryStorage : globalContext.localStorage;
}
/**
* Returns the storage namespace, which is the application's `modulePrefix`.
* @return {string}
*/
get namespace() {
const config = getOwner(this).resolveRegistration('config:environment');
return config.modulePrefix;
}
/**
* Returns a fully qualified key from the passed string. A fully qualified
* key includes the namespace.
* @param {string} key
* @return {string} fully qualified key
*/
qualifiedKey(key) {
return `${this.namespace}:${key}`;
}
/**
* Gets a value from storage.
* @param {string} key
* @return {any}
*/
getItem(key) {
const value = this.storage.getItem(this.qualifiedKey(key));
if (value) return JSON.parse(value);
}
/**
* Sets a value to storage.
* @param {string} key
*/
setItem(key, value) {
this.storage.setItem(this.qualifiedKey(key), JSON.stringify(value));
}
/**
* Removes a value from storage.
* @param {string} key
*/
removeItem(key) {
this.storage.removeItem(this.qualifiedKey(key));
}
}
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/adapters/application';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/account';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/auth-method';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/auth-token';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/base';
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-account-attributes';
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-host-attributes';
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-host-set';
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-principal';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-scope';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-string';
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-target-attributes';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/group';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/host-catalog';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/host-set';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/host';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/role';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/scope';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/session';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/target';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/user';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/account';
@@ -0,0 +1 @@
export { default } from 'api/serializers/application';
@@ -0,0 +1 @@
export { default } from 'api/serializers/fragment-host-set';
@@ -0,0 +1 @@
export { default } from 'api/serializers/fragment-principal';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/group';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/host-set';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/host';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/role';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/session';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/target';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/user';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/services/storage';
Loaded 100 of 1122 files, more files were not shown because too many files have changed in this diff. Show more