1122 files changed
+74733
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/**/*
|
||||
@@ -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"
|
||||
@@ -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 }}"
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,154 @@
|
||||
# Boundary UI Monorepo
|
||||
|
||||

|
||||
|
||||
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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: 'octane'
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
||||
@@ -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.
|
||||
Whitespace-only changes.
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {}
|
||||
@@ -0,0 +1,3 @@
|
||||
import GeneratedAuthTokenModel from '../generated/models/auth-token';
|
||||
|
||||
export default class AuthTokenModel extends GeneratedAuthTokenModel {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class HostSerializer extends ApplicationSerializer {
|
||||
|
||||
// =properties
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
serializeScopeID = false;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/application';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/account';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/auth-method';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/auth-token';
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-scope';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-string';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-target-attributes';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/group';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/host-catalog';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/host-set';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/host';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/role';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/scope';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/session';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/target';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/user';
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/group';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/host-set';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/host';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/role';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/session';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/target';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/user';
|
||||
@@ -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
Reference in new issue
Block a user