├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── LICENSE.md ├── README.md ├── addon ├── -private │ ├── coalesce-errors.ts │ ├── document-rules │ │ ├── data-is-valid.ts │ │ ├── included-is-valid.ts │ │ ├── included-must-have-data.ts │ │ ├── it-cant-have-both.ts │ │ ├── it-exists.ts │ │ ├── it-has-at-least-one-non-null.ts │ │ ├── it-has-at-least-one.ts │ │ ├── it-has-no-unknown-members.ts │ │ └── it-has-valid-errors.ts │ ├── errors │ │ ├── attribute-error.ts │ │ ├── document-error.ts │ │ ├── links-error.ts │ │ ├── meta-error.ts │ │ ├── reference-error.ts │ │ ├── relationship-error.ts │ │ ├── resource-error.ts │ │ └── validation-error.ts │ ├── meta-rules │ │ └── it-has-required-sibling.ts │ ├── utils │ │ ├── about-an-oxford-comma.ts │ │ ├── assert-member-format.ts │ │ ├── assert-type-format.ts │ │ ├── is-camel.ts │ │ ├── is-dasherized.ts │ │ ├── is-normalized-type.ts │ │ ├── is-plain-object.ts │ │ ├── member-defined-and-not-null.ts │ │ ├── member-defined.ts │ │ ├── member-present.ts │ │ ├── normalize-type.ts │ │ └── type-of.ts │ ├── validate-attributes.ts │ ├── validate-document.ts │ ├── validate-jsonapi-member.ts │ ├── validate-links.ts │ ├── validate-meta.ts │ ├── validate-reference.ts │ ├── validate-relationships.ts │ ├── validate-resource.ts │ └── validator.ts └── setup-ember-data-validations.ts ├── app └── initializers │ └── json-api-validator.ts ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── .eslintrc.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── config │ │ │ └── environment.d.ts │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── animal.js │ │ │ ├── dog.js │ │ │ ├── flying-dog.js │ │ │ ├── person.js │ │ │ └── pet.js │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── application.hbs │ │ │ └── components │ │ │ └── .gitkeep │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── deep-copy.ts │ ├── deep-merge.ts │ ├── destroy-app.ts │ ├── module-for-acceptance.ts │ ├── resolver.ts │ └── start-app.ts ├── index.html ├── integration │ └── .gitkeep ├── test-helper.js └── unit │ ├── invalid-document-test.ts │ └── invalid-resource-test.ts ├── tsconfig.json ├── types ├── dummy │ └── index.d.ts ├── ember-data.d.ts └── qunit-assert-helpers.d.ts ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | 12 | # misc 13 | /coverage/ 14 | 15 | # ember-try 16 | /.node_modules.ember-try/ 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | }, 19 | overrides: [ 20 | // node files 21 | { 22 | files: [ 23 | 'ember-cli-build.js', 24 | 'index.js', 25 | 'testem.js', 26 | 'blueprints/*/index.js', 27 | 'config/**/*.js', 28 | 'tests/dummy/config/**/*.js' 29 | ], 30 | excludedFiles: [ 31 | 'addon/**', 32 | 'addon-test-support/**', 33 | 'app/**', 34 | 'tests/dummy/app/**' 35 | ], 36 | parserOptions: { 37 | sourceType: 'script', 38 | ecmaVersion: 2015 39 | }, 40 | env: { 41 | browser: false, 42 | node: true 43 | }, 44 | plugins: ['node'], 45 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 46 | // add your custom rules and overrides for node files here 47 | }) 48 | } 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/ 15 | /libpeerconnection.log 16 | /npm-debug.log* 17 | /testem.log 18 | /yarn-error.log 19 | 20 | # ember-try 21 | /.node_modules.ember-try/ 22 | /bower.json.ember-try 23 | /package.json.ember-try 24 | 25 | # Emacs files 26 | *~ 27 | \#*\# 28 | .\#* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | yarn.lock 18 | 19 | # ember-try 20 | .node_modules.ember-try/ 21 | bower.json.ember-try 22 | package.json.ember-try 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "6" 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | yarn: true 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | matrix: 22 | # we recommend new addons test the current and previous LTS 23 | # as well as latest stable release (bonus points to beta/canary) 24 | - EMBER_TRY_SCENARIO=ember-lts-2.12 25 | - EMBER_TRY_SCENARIO=ember-lts-2.16 26 | - EMBER_TRY_SCENARIO=ember-lts-2.18 27 | - EMBER_TRY_SCENARIO=ember-release 28 | - EMBER_TRY_SCENARIO=ember-beta 29 | - EMBER_TRY_SCENARIO=ember-canary 30 | - EMBER_TRY_SCENARIO=ember-default 31 | 32 | matrix: 33 | fast_finish: true 34 | allow_failures: 35 | - env: EMBER_TRY_SCENARIO=ember-canary 36 | 37 | before_install: 38 | - curl -o- -L https://yarnpkg.com/install.sh | bash 39 | - export PATH=$HOME/.yarn/bin:$PATH 40 | 41 | install: 42 | - yarn install --no-lockfile --non-interactive 43 | 44 | script: 45 | - yarn lint:js 46 | # Usually, it's ok to finish the test scenario without reverting 47 | # to the addon's original dependency state, skipping "cleanup". 48 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup 49 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @ember-data/json-api-validator 2 | ============================================================================== 3 | 4 | This package provides `json-api` validation utilities for applications built 5 | with `ember-data`. 6 | 7 | `ember-data` expects users to normalize API payloads into `json-api` via serializers 8 | or other means before the data is pushed to the store. This is true for all requests, 9 | both manual and those made via "finders" such as `query` or `findRecord`. 10 | 11 | Most obscure "why doesn't ember-data work" errors can be caught earlier with more 12 | clarity by using this package to validate the payloads given to the store. 13 | 14 | Installation 15 | ------------------------------------------------------------------------------ 16 | 17 | ``` 18 | ember install @ember-data/json-api-validator 19 | ``` 20 | 21 | 22 | Usage 23 | ------------------------------------------------------------------------------ 24 | 25 | This addon automatically hooks into the `ember-data` store and validates data 26 | pushed into it against the `json-api` spec taking into account available `model` 27 | definitions and `ember-data` quirks (member names must be `camelCase`, type must 28 | be singular and dasherized). 29 | 30 | Unknown attributes and relationships are ignored by default; however, you can choose 31 | to warn or assert unknown attributes or relationships instead. 32 | 33 | For relationships, it validates that synchronous relationships are indeed included 34 | when specified. You may choose to make this a warning instead. 35 | 36 | For polymorphic associations, it validates that pushed types are indeed polymorphic 37 | sub-classes of the base type. 38 | 39 | In addition to automatically adding validation to the store, this addon provides 40 | an additional `validateJsonApiDocument` method on the store. You may also import 41 | and use the individual validation methods to validate generic `json-api` payloads 42 | without the additional context of available `Model` schemas. 43 | 44 | Contributing 45 | ------------------------------------------------------------------------------ 46 | 47 | ### Installation 48 | 49 | * `git clone ` 50 | * `cd @ember-data/json-api-validator` 51 | * `yarn install` 52 | 53 | ### Linting 54 | 55 | * `yarn lint:js` 56 | * `yarn lint:js --fix` 57 | 58 | ### Running tests 59 | 60 | * `ember test` – Runs the test suite on the current Ember version 61 | * `ember test --server` – Runs the test suite in "watch mode" 62 | * `ember try:each` – Runs the test suite against multiple Ember versions 63 | 64 | ### Running the dummy application 65 | 66 | * `ember serve` 67 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 68 | 69 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 70 | 71 | License 72 | ------------------------------------------------------------------------------ 73 | 74 | This project is licensed under the [MIT License](LICENSE.md). 75 | -------------------------------------------------------------------------------- /addon/-private/coalesce-errors.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './errors/validation-error'; 2 | import { warn } from '@ember/debug'; 3 | 4 | export default function coalesceAndThrowErrors(issues) { 5 | let { errors, warnings } = issues; 6 | let error; 7 | let warning; 8 | 9 | if (errors.length === 0 && warnings.length === 0) { 10 | return; 11 | } 12 | 13 | if (errors.length === 1) { 14 | error = errors[0]; 15 | } else { 16 | let errorMessage = 'The data provided failed json-api validation.' + 17 | ' The detected errors are listed below. Detailed stack traces for each error can' + 18 | ' be found in the errors array of this error object.\n\n\n'; 19 | 20 | for (let i = 0; i < errors.length; i++) { 21 | errorMessage += '\n' + i + ')\t' + errors[i].message; 22 | } 23 | 24 | error = new ValidationError(errorMessage); 25 | error.errors = errors; 26 | } 27 | 28 | if (warnings.length === 1) { 29 | warning = warnings[0].message 30 | } else { 31 | warning = 'The data provided encountered likely json-api validation.' + 32 | ' The potential errors are listed below.\n\n\n'; 33 | 34 | for (let i = 0; i < warnings.length; i++) { 35 | warning += '\n' + i + ')\t' + warnings[i].message; 36 | } 37 | } 38 | 39 | if (warnings.length) { 40 | warn(warning, false, { 41 | id: 'json-api-validater-warnings' 42 | }); 43 | } 44 | 45 | if (errors.length) { 46 | throw error; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /addon/-private/document-rules/data-is-valid.ts: -------------------------------------------------------------------------------- 1 | import memberDefined from '../utils/member-defined'; 2 | import validateResource from '../validate-resource'; 3 | 4 | import { IValidationContext } from 'ember-data'; 5 | 6 | /** 7 | * TODO update the below because resource-identifiers probably can have links 8 | * but ember-data doesn't allow resource-identifiers in data/included yet anyway 9 | * 10 | * The `data` key of a json-api document must contain either 11 | * 12 | * 1) an object representing either 13 | * a) a resource-identifier (Reference) with type, id, and optional meta 14 | * b) a resource-object (Resource) with type, id and optional meta, links, attributes, and relationships 15 | * 2) an array consisting of either 16 | * c) entirely resource-identifiers 17 | * d) entirely resource-objects 18 | * 19 | * Because of ambiguity in the json-api spec allowing for resource-object's without attributes and relationships 20 | * to look suspiciously similar to resource-identifiers we define that a resource-object MUST have AT LEAST ONE 21 | * of `attributes`, `relationships`, or `links`. 22 | * 23 | * Spec - ResourceObject: http://jsonapi.org/format/#document-resource-objects 24 | * Spec - ResourceIdentifier: http://jsonapi.org/format/#document-resource-identifier-objects 25 | * 26 | * This also means that a resource-identifier MUST NOT have links, which is supported by the spec but appears 27 | * to be violated in many real-world applications. 28 | * 29 | * For further reading on how we validate the structure of a resource see the `validateResource` method. 30 | * 31 | * @param validator 32 | * @param document 33 | * @param errors 34 | * @param path 35 | * @returns {boolean} 36 | */ 37 | export default function dataIsValid({ validator, document, issues, path }: IValidationContext) { 38 | if (!memberDefined(document, 'data')) { 39 | return true; 40 | } 41 | 42 | if (document.data === null) { 43 | return true; 44 | } else if (Array.isArray(document.data)) { 45 | let hasError = false; 46 | document.data.forEach((resource, i) => { 47 | let didError = validateResource({ 48 | validator, 49 | document, 50 | target: resource, 51 | issues, 52 | path: `${path}.data[${i}]` 53 | }); 54 | 55 | hasError = hasError || didError; 56 | }); 57 | 58 | return hasError; 59 | } else { 60 | return validateResource({ 61 | validator, 62 | document, 63 | target: document.data, 64 | issues, 65 | path: path + '.data' 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /addon/-private/document-rules/included-is-valid.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | import memberDefined from '../utils/member-defined'; 3 | import validateResource from '../validate-resource'; 4 | 5 | import { IValidationContext } from 'ember-data'; 6 | 7 | /** 8 | * The `included` key of a json-api document MUST be an Array if present and MUST contain only 9 | * resource-objects (Resource). 10 | * 11 | * Every resource-object in included MUST be linked to 12 | * by another resource-object within the payload, see: 13 | * 14 | * http://jsonapi.org/format/#document-compound-documents 15 | * 16 | * However, exceptions are made for for sparse fieldsets 17 | * which makes this difficult to enforce. 18 | * 19 | * @param validator 20 | * @param document 21 | * @param errors 22 | * @param path 23 | * @returns {boolean} 24 | */ 25 | export default function includedIsValid({ validator, document, issues, path }: IValidationContext) { 26 | if (!memberDefined(document, 'included')) { 27 | return true; 28 | } 29 | 30 | if (Array.isArray(document.included)) { 31 | let hasError = false; 32 | document.included.forEach((resource, i) => { 33 | let didError = validateResource({ 34 | validator, 35 | document, 36 | target: resource, 37 | issues, 38 | path: `${path}.included[${i}]` 39 | }); 40 | 41 | hasError = hasError || didError; 42 | }); 43 | 44 | return hasError; 45 | } 46 | 47 | issues.errors.push(new DocumentError({ 48 | validator, 49 | document, 50 | path, 51 | value: document.included, 52 | member: 'included', 53 | code: DOCUMENT_ERROR_TYPES.INVALID_INCLUDED_VALUE 54 | })); 55 | 56 | return false; 57 | } 58 | -------------------------------------------------------------------------------- /addon/-private/document-rules/included-must-have-data.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | import memberPresent from '../utils/member-present'; 3 | import memberDefined from '../utils/member-defined'; 4 | 5 | import { IValidationContext } from 'ember-data'; 6 | 7 | /** 8 | * Spec: http://jsonapi.org/format/#document-top-level 9 | * 10 | * @param validator 11 | * @param document 12 | * @param issues 13 | * @param path 14 | * @returns {boolean} 15 | */ 16 | export default function includedMustHaveData({ validator, document, issues, path }: IValidationContext) { 17 | let { errors } = issues; 18 | 19 | if (memberPresent(document, 'included') && !memberDefined(document, 'data')) { 20 | let issue = new DocumentError({ 21 | code: DOCUMENT_ERROR_TYPES.DISALLOWED_INCLUDED_MEMBER, 22 | path, 23 | document, 24 | validator, 25 | value: 'included' 26 | }); 27 | 28 | errors.push(issue); 29 | 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-cant-have-both.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | import memberPresent from '../utils/member-present'; 3 | 4 | import { IValidationContext } from 'ember-data'; 5 | 6 | /** 7 | * Validates that a document does not have both data and errors 8 | * 9 | * @param validator 10 | * @param document 11 | * @param issues 12 | * @param path 13 | */ 14 | export default function itCantHaveBoth({ validator, document, issues, path }: IValidationContext) { 15 | let { errors } = issues; 16 | 17 | if (memberPresent(document, 'data') && memberPresent(document, 'errors')) { 18 | errors.push( 19 | new DocumentError({ 20 | document, 21 | path, 22 | code: DOCUMENT_ERROR_TYPES.DISALLOWED_DATA_MEMBER, 23 | validator, 24 | value: ['data', 'errors'], 25 | }) 26 | ); 27 | 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-exists.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | 3 | import { IValidationContext } from 'ember-data'; 4 | 5 | /** 6 | * Validates that a document is an object 7 | * 8 | * @param validator 9 | * @param document 10 | * @param issues 11 | * @param path 12 | */ 13 | export default function documentExists({ validator, document, issues, path }: IValidationContext) { 14 | let { errors } = issues; 15 | let type = typeof document; 16 | 17 | if (type !== 'object' || document === null || document instanceof Date) { 18 | errors.push( 19 | new DocumentError({ 20 | document, 21 | path, 22 | code: DOCUMENT_ERROR_TYPES.INVALID_DOCUMENT, 23 | validator, 24 | value: type, 25 | }) 26 | ); 27 | 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-has-at-least-one-non-null.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | import memberDefined from '../utils/member-defined'; 3 | import memberDefinedAndNotNull from '../utils/member-defined-and-not-null'; 4 | 5 | import { IValidationContext } from 'ember-data'; 6 | 7 | const AT_LEAST_ONE = ['data', 'meta', 'errors']; 8 | 9 | /** 10 | * Validates that a document has at least one of 11 | * the following keys: `data`, `meta`, and `errors` 12 | * and that the key is non-null 13 | * 14 | * @param validator 15 | * @param document 16 | * @param issues 17 | * @param path 18 | */ 19 | export default function itHasAtLeastOneNonNull({ 20 | validator, 21 | document, 22 | issues, 23 | path, 24 | }: IValidationContext) { 25 | let { errors } = issues; 26 | let nullMembers = []; 27 | 28 | for (let i = 0; i < AT_LEAST_ONE.length; i++) { 29 | let neededKey = AT_LEAST_ONE[i]; 30 | 31 | if (memberDefinedAndNotNull(document, neededKey)) { 32 | return true; 33 | // possibly should be a presence check not a defined check 34 | } else if (memberDefined(document, neededKey)) { 35 | nullMembers.push(neededKey); 36 | } 37 | } 38 | 39 | if (nullMembers.length) { 40 | errors.push( 41 | new DocumentError({ 42 | document, 43 | path, 44 | code: DOCUMENT_ERROR_TYPES.NULL_MANDATORY_MEMBER, 45 | validator, 46 | key: nullMembers, 47 | value: AT_LEAST_ONE, 48 | }) 49 | ); 50 | } 51 | 52 | return false; 53 | } 54 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-has-at-least-one.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | import memberDefined from '../utils/member-defined'; 3 | 4 | import { IValidationContext } from 'ember-data'; 5 | 6 | const AT_LEAST_ONE = ['data', 'meta', 'errors']; 7 | 8 | /** 9 | * Validates that a document has at least one of 10 | * the following keys: `data`, `meta`, and `errors`. 11 | * 12 | * @param validator 13 | * @param document 14 | * @param issues 15 | * @param path 16 | */ 17 | export default function itHasAtLeastOne({ validator, document, issues, path }: IValidationContext) { 18 | let { errors } = issues; 19 | 20 | for (let i = 0; i < AT_LEAST_ONE.length; i++) { 21 | let neededKey = AT_LEAST_ONE[i]; 22 | 23 | if (memberDefined(document, neededKey)) { 24 | return true; 25 | } 26 | } 27 | 28 | errors.push( 29 | new DocumentError({ 30 | document, 31 | path, 32 | code: DOCUMENT_ERROR_TYPES.MISSING_MANDATORY_MEMBER, 33 | validator, 34 | value: AT_LEAST_ONE, 35 | }) 36 | ); 37 | 38 | return false; 39 | } 40 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-has-no-unknown-members.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from '../errors/document-error'; 2 | 3 | import { IValidationContext } from 'ember-data'; 4 | 5 | const AT_LEAST_ONE = ['data', 'meta', 'errors']; 6 | 7 | const OPTIONAL_KEYS = ['jsonapi', 'links', 'included']; 8 | 9 | /** 10 | * 11 | * @param validator 12 | * @param document 13 | * @param issues 14 | * @param path 15 | * @returns {boolean} 16 | */ 17 | export default function itHasNoUnknownMembers({ 18 | validator, 19 | document, 20 | issues, 21 | path, 22 | }: IValidationContext) { 23 | let { warnings, errors } = issues; 24 | let { strictMode } = validator; 25 | let hasError = false; 26 | 27 | Object.keys(document).forEach(key => { 28 | if (OPTIONAL_KEYS.indexOf(key) === -1 && AT_LEAST_ONE.indexOf(key) === -1) { 29 | let issue = new DocumentError({ 30 | code: DOCUMENT_ERROR_TYPES.UNKNOWN_MEMBER, 31 | document, 32 | path, 33 | validator, 34 | value: key, 35 | }); 36 | 37 | strictMode === true ? errors.push(issue) : warnings.push(issue); 38 | hasError = true; 39 | } 40 | }); 41 | 42 | return strictMode === true || !hasError; 43 | } 44 | -------------------------------------------------------------------------------- /addon/-private/document-rules/it-has-valid-errors.ts: -------------------------------------------------------------------------------- 1 | import memberPresent from "../utils/member-present"; 2 | 3 | /** 4 | * MUST be an array of error-objects 5 | * 6 | * See: http://jsonapi.org/format/#error-objects 7 | * 8 | * @param validator 9 | * @param document 10 | * @param errors 11 | * @param path 12 | * @returns {boolean} 13 | */ 14 | export default function validateErrors({ /*validator,*/ document, /*errors,*//*path*/ }) { 15 | if (memberPresent(document, "errors")) { 16 | return !Array.isArray(document.errors); 17 | } 18 | 19 | return true; 20 | } 21 | -------------------------------------------------------------------------------- /addon/-private/errors/attribute-error.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError, createNiceErrorMessage, uniqueErrorId } from './validation-error'; 2 | 3 | export const ATTRIBUTE_ERROR_TYPES = { 4 | INVALID_HASH: uniqueErrorId(), 5 | UNKNOWN_ATTRIBUTE: uniqueErrorId(), 6 | UNDEFINED_VALUE: uniqueErrorId(), 7 | INCORRECT_VALUE_TYPE: uniqueErrorId(), 8 | }; 9 | 10 | export class AttributeError extends ValidationError { 11 | constructor(errorType, type, propertyName, value, path) { 12 | const errorLocation = createNiceErrorMessage( 13 | propertyName, 14 | value, 15 | errorType === ATTRIBUTE_ERROR_TYPES.INVALID_HASH ? path : (path ? path + '.attributes' : 'attributes'), 16 | errorType === ATTRIBUTE_ERROR_TYPES.UNKNOWN_ATTRIBUTE, 17 | ); 18 | const error = buildPrimaryAttributeErrorMessage(errorType, type, propertyName, value); 19 | const message = error + errorLocation; 20 | super(message); 21 | } 22 | } 23 | 24 | function buildPrimaryAttributeErrorMessage(errorType, type, propertyName, value) { 25 | switch (errorType) { 26 | case ATTRIBUTE_ERROR_TYPES.INVALID_HASH: 27 | return `Expected the attributes hash for a resource to be an object, found '${value}' for type '${type}'`; 28 | 29 | case ATTRIBUTE_ERROR_TYPES.UNKNOWN_ATTRIBUTE: 30 | return `The attribute '${propertyName}' does not exist on the schema for type '${type}'`; 31 | 32 | case ATTRIBUTE_ERROR_TYPES.UNDEFINED_VALUE: 33 | return `undefined is not a valid value for the attribute '${propertyName}' on a resource of type '${type}'. To indicate empty, deleted, or un-set use null.`; 34 | 35 | case ATTRIBUTE_ERROR_TYPES.INCORRECT_VALUE_TYPE: 36 | return `An unclassified error occurred while validating the attribute '${propertyName}' on a resource of type '${type}'`; 37 | 38 | default: 39 | return `An unclassified error occurred while validating the attribute '${propertyName}' for a resource of type '${type}'`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /addon/-private/errors/document-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NICE_ERROR_TYPES, 3 | ValidationError, 4 | createNiceErrorMessage, 5 | uniqueErrorId, 6 | } from './validation-error'; 7 | import isPlainObject from '../utils/is-plain-object'; 8 | import aboutAnOxfordComma from '../utils/about-an-oxford-comma'; 9 | import typeOf from '../utils/type-of'; 10 | 11 | export const DOCUMENT_ERROR_TYPES = { 12 | INVALID_DOCUMENT: uniqueErrorId(), 13 | MISSING_MANDATORY_MEMBER: uniqueErrorId(), 14 | NULL_MANDATORY_MEMBER: uniqueErrorId(), 15 | DISALLOWED_DATA_MEMBER: uniqueErrorId(), 16 | DISALLOWED_INCLUDED_MEMBER: uniqueErrorId(), 17 | UNKNOWN_MEMBER: uniqueErrorId(), 18 | VALUE_MUST_BE_OBJECT: uniqueErrorId(), 19 | VERSION_MUST_BE_STRING: uniqueErrorId(), 20 | MISSING_VERSION: uniqueErrorId(), 21 | INVALID_INCLUDED_VALUE: uniqueErrorId(), 22 | }; 23 | 24 | export class DocumentError extends ValidationError { 25 | constructor(options) { 26 | let { value, path, document } = options; 27 | const errorLocation = createNiceErrorMessage({ 28 | key: Array.isArray(value) ? '' : value, 29 | value: isPlainObject(document) ? JSON.stringify(document) : document, 30 | path, 31 | code: NICE_ERROR_TYPES.OBJECT_ERROR, 32 | }); 33 | const error = buildDocumentErrorMessage(options); 34 | const message = error + errorLocation; 35 | super(message); 36 | } 37 | } 38 | 39 | function buildDocumentErrorMessage(options) { 40 | let { value, code, document, member, path } = options; 41 | 42 | switch (code) { 43 | case DOCUMENT_ERROR_TYPES.INVALID_DOCUMENT: 44 | return `Value of type "${typeOf( 45 | document 46 | )}" is not a valid json-api document.`; 47 | 48 | case DOCUMENT_ERROR_TYPES.MISSING_MANDATORY_MEMBER: 49 | return `A json-api document MUST contain one of ${aboutAnOxfordComma( 50 | value 51 | )} as a member.`; 52 | 53 | case DOCUMENT_ERROR_TYPES.NULL_MANDATORY_MEMBER: 54 | return `A json-api document MUST contain one of ${aboutAnOxfordComma( 55 | value 56 | )} as a non-null member.`; 57 | 58 | case DOCUMENT_ERROR_TYPES.DISALLOWED_DATA_MEMBER: 59 | return 'A json-api document MUST NOT contain both `data` and `errors` as a members.'; 60 | 61 | case DOCUMENT_ERROR_TYPES.DISALLOWED_INCLUDED_MEMBER: 62 | return 'A json-api document MUST NOT contain `included` as a member unless `data` is also present.'; 63 | 64 | case DOCUMENT_ERROR_TYPES.UNKNOWN_MEMBER: 65 | if (member === 'jsonapi') { 66 | return `'${value}' is not a valid member of the jsonapi object on a json-api document.`; 67 | } 68 | return `'${value}' is not a valid member of a json-api document.`; 69 | 70 | case DOCUMENT_ERROR_TYPES.MISSING_VERSION: 71 | return `expected a 'version' member to be present in the 'document.jsonapi' object`; 72 | 73 | case DOCUMENT_ERROR_TYPES.VERSION_MUST_BE_STRING: 74 | return `expected the 'version' member present in the 'document.jsonapi' object to be a string, found value of type ${typeOf( 75 | value 76 | )}`; 77 | 78 | case DOCUMENT_ERROR_TYPES.VALUE_MUST_BE_OBJECT: 79 | return `'${path}.${member}' MUST be an object if present, found value of type ${typeOf( 80 | value 81 | )}`; 82 | 83 | case DOCUMENT_ERROR_TYPES.INVALID_INCLUDED_VALUE: 84 | return `expected document.included to be an Array, instead found value of type ${typeOf( 85 | value 86 | )}`; 87 | } 88 | 89 | return 'DocumentError'; 90 | } 91 | -------------------------------------------------------------------------------- /addon/-private/errors/links-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NICE_ERROR_TYPES, 3 | ValidationError, 4 | createNiceErrorMessage, 5 | uniqueErrorId, 6 | } from './validation-error'; 7 | import isPlainObject from '../utils/is-plain-object'; 8 | import typeOf from '../utils/type-of'; 9 | 10 | import { Value } from 'json-typescript'; 11 | import { Document } from 'jsonapi-typescript'; 12 | 13 | export const LINKS_ERROR_TYPES = { 14 | UNKNOWN_MEMBER: uniqueErrorId(), 15 | INVALID_MEMBER: uniqueErrorId(), 16 | VALUE_MUST_BE_OBJECT: uniqueErrorId(), 17 | OBJECT_MUST_NOT_BE_EMPTY: uniqueErrorId(), 18 | }; 19 | 20 | export interface ILinksErrorOptions { 21 | value: Value; 22 | path: string; 23 | document: Document; 24 | code: string | number; 25 | member: any; 26 | } 27 | 28 | export class LinksError extends ValidationError { 29 | constructor(options: ILinksErrorOptions) { 30 | let { value, path, document } = options; 31 | const errorLocation = createNiceErrorMessage({ 32 | key: Array.isArray(value) ? '' : value, 33 | value: isPlainObject(document) ? JSON.stringify(document) : document, 34 | path, 35 | code: NICE_ERROR_TYPES.OBJECT_ERROR, 36 | }); 37 | const error = buildMetaErrorMessage(options); 38 | const message = error + errorLocation; 39 | super(message); 40 | } 41 | } 42 | 43 | function buildMetaErrorMessage(options: ILinksErrorOptions) { 44 | let { value, code, member, path } = options; 45 | 46 | switch (code) { 47 | case LINKS_ERROR_TYPES.INVALID_MEMBER: 48 | return `'${path}.${member}' MUST NOT be undefined.`; 49 | 50 | case LINKS_ERROR_TYPES.VALUE_MUST_BE_OBJECT: 51 | return `'${path}.${member}' MUST be an object when present: found value of type ${typeOf( 52 | value 53 | )}`; 54 | 55 | case LINKS_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY: 56 | return `'${path}.${member}' MUST have at least one member: found an empty object.`; 57 | } 58 | 59 | return 'DocumentError'; 60 | } 61 | -------------------------------------------------------------------------------- /addon/-private/errors/meta-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NICE_ERROR_TYPES, 3 | ValidationError, 4 | createNiceErrorMessage, 5 | uniqueErrorId, 6 | } from './validation-error'; 7 | import aboutAnOxfordComma from '../utils/about-an-oxford-comma'; 8 | import isPlainObject from '../utils/is-plain-object'; 9 | import typeOf from '../utils/type-of'; 10 | import { IErrorOptions } from 'ember-data'; 11 | 12 | export const META_ERROR_TYPES = { 13 | DISALLOWED_SOLITARY_META_MEMBER: uniqueErrorId(), 14 | INVALID_MEMBER: uniqueErrorId(), 15 | VALUE_MUST_BE_OBJECT: uniqueErrorId(), 16 | OBJECT_MUST_NOT_BE_EMPTY: uniqueErrorId(), 17 | }; 18 | 19 | export class MetaError extends ValidationError { 20 | constructor(options: IErrorOptions) { 21 | let { value, path, document } = options; 22 | const errorLocation = createNiceErrorMessage({ 23 | key: Array.isArray(value) ? '' : value, 24 | value: isPlainObject(document) ? JSON.stringify(document) : document, 25 | path, 26 | code: NICE_ERROR_TYPES.OBJECT_ERROR, 27 | }); 28 | const error = buildMetaErrorMessage(options); 29 | const message = error + errorLocation; 30 | super(message); 31 | } 32 | } 33 | 34 | function buildMetaErrorMessage(options) { 35 | let { value, code, member, path, expectedValue } = options; 36 | 37 | switch (code) { 38 | case META_ERROR_TYPES.DISALLOWED_SOLITARY_META_MEMBER: 39 | return `'${path}.${member}' MUST NOT be the only member of '${path}. Expected ${aboutAnOxfordComma( 40 | expectedValue 41 | )} as a sibling.`; 42 | 43 | case META_ERROR_TYPES.INVALID_MEMBER: 44 | return `'${path}.${member}' MUST NOT be undefined.`; 45 | 46 | case META_ERROR_TYPES.VALUE_MUST_BE_OBJECT: 47 | return `'${path}.${member}' MUST be an object when present: found value of type ${typeOf( 48 | value 49 | )}`; 50 | 51 | case META_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY: 52 | return `'${path}.${member}' MUST have at least one member: found an empty object.`; 53 | } 54 | 55 | return 'DocumentError'; 56 | } 57 | -------------------------------------------------------------------------------- /addon/-private/errors/reference-error.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError, createNiceErrorMessage, uniqueErrorId } from './validation-error'; 2 | 3 | import { Value } from 'json-typescript'; 4 | 5 | export const REFERENCE_ERROR_TYPES = { 6 | REFERENCE_MISSING: uniqueErrorId(), 7 | REFERENCE_IS_ARRAY: uniqueErrorId(), 8 | INVALID_REFERENCE: uniqueErrorId(), 9 | UNEXPECTED_KEY: uniqueErrorId(), 10 | MISSING_KEY: uniqueErrorId(), 11 | MISSING_INFO: uniqueErrorId(), 12 | INVALID_ID_VALUE: uniqueErrorId(), 13 | INVALID_TYPE_VALUE: uniqueErrorId(), 14 | INVALID_TYPE_FORMAT: uniqueErrorId(), 15 | UNKNOWN_SCHEMA: uniqueErrorId() 16 | }; 17 | 18 | export class ReferenceError extends ValidationError { 19 | constructor(errorType: number, type: string, propertyName: string, value: Value, path: string) { 20 | let errorLocation = ''; 21 | 22 | if ( 23 | errorType !== REFERENCE_ERROR_TYPES.REFERENCE_MISSING && 24 | errorType !== REFERENCE_ERROR_TYPES.REFERENCE_IS_ARRAY && 25 | errorType !== REFERENCE_ERROR_TYPES.MISSING_INFO 26 | ) { 27 | errorLocation = createNiceErrorMessage( 28 | propertyName, 29 | value, 30 | path, 31 | false, 32 | ); 33 | } 34 | const error = buildPrimaryResourceErrorMessage(errorType, type, propertyName || path, value); 35 | const message = error + errorLocation; 36 | super(message); 37 | } 38 | } 39 | 40 | function buildPrimaryResourceErrorMessage(errorType: number, type: string, propertyName: string, value: Value) { 41 | switch (errorType) { 42 | case REFERENCE_ERROR_TYPES.REFERENCE_MISSING: 43 | return `Expected to receive a json-api reference${propertyName ? ' at ' + propertyName : ''} but instead found '${value}'.`; 44 | 45 | case REFERENCE_ERROR_TYPES.REFERENCE_IS_ARRAY: 46 | return `Expected to receive a single json-api reference${propertyName ? ' at ' + propertyName : ''} but instead found an Array.`; 47 | 48 | case REFERENCE_ERROR_TYPES.INVALID_REFERENCE: 49 | return `Expected to receive a json-api reference${propertyName ? ' at ' + propertyName : ''} but instead found '${value}'.`; 50 | 51 | case REFERENCE_ERROR_TYPES.UNEXPECTED_KEY: 52 | return `Unexpected key in payload: ${propertyName}`; 53 | 54 | case REFERENCE_ERROR_TYPES.MISSING_KEY: 55 | return `Missing mandatory key in payload: ${propertyName}`; 56 | 57 | case REFERENCE_ERROR_TYPES.MISSING_INFO: 58 | return `In addition to 'type' and 'id', a reference needs at least one of the following keys: ${value}`; 59 | 60 | case REFERENCE_ERROR_TYPES.INVALID_ID_VALUE: 61 | case REFERENCE_ERROR_TYPES.INVALID_TYPE_VALUE: 62 | return `Resource.${propertyName} must be a string, found ${value}`; 63 | 64 | case REFERENCE_ERROR_TYPES.INVALID_TYPE_FORMAT: 65 | return `Expected reference type to be dasherized, found '${value}' instead of '${type}'.`; 66 | 67 | case REFERENCE_ERROR_TYPES.UNKNOWN_SCHEMA: 68 | return `Unknown reference, no schema was found for type '${value}'`; 69 | 70 | default: 71 | return `An unclassified error occurred while validating the relationship '${propertyName}' for a resource of type '${type}'`; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /addon/-private/errors/relationship-error.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError, createNiceErrorMessage, uniqueErrorId } from './validation-error'; 2 | 3 | import { Value } from 'json-typescript'; 4 | 5 | export const RELATIONSHIP_ERROR_TYPES = { 6 | INVALID_HASH: uniqueErrorId(), 7 | UNKNOWN_RELATIONSHIP: uniqueErrorId(), 8 | UNDEFINED_VALUE: uniqueErrorId(), 9 | INCORRECT_VALUE_TYPE: uniqueErrorId(), 10 | MALFORMATTED_TYPE: uniqueErrorId(), 11 | }; 12 | 13 | export class RelationshipError extends ValidationError { 14 | constructor(errorType: number, type: string, propertyName: string, value: Value, path: string) { 15 | const errorLocation = createNiceErrorMessage( 16 | propertyName, 17 | value, 18 | errorType === RELATIONSHIP_ERROR_TYPES.INVALID_HASH ? path : (path ? path + '.relationships' : 'relationships'), 19 | errorType === RELATIONSHIP_ERROR_TYPES.UNKNOWN_RELATIONSHIP 20 | ); 21 | const error = buildPrimaryRelationshipErrorMessage(errorType, type, propertyName, value); 22 | const message = error + errorLocation; 23 | super(message); 24 | } 25 | } 26 | 27 | function buildPrimaryRelationshipErrorMessage(errorType: number, type: string, propertyName: string, value: Value) { 28 | switch (errorType) { 29 | case RELATIONSHIP_ERROR_TYPES.INVALID_HASH: 30 | return `Expected the relationships hash for a resource to be an object, found '${value}' for type '${type}'`; 31 | 32 | case RELATIONSHIP_ERROR_TYPES.UNKNOWN_RELATIONSHIP: 33 | return `The relationship '${propertyName}' does not exist on the schema for type '${type}'`; 34 | 35 | case RELATIONSHIP_ERROR_TYPES.UNDEFINED_VALUE: 36 | return `undefined is not a valid value for the relationship '${propertyName}' on a resource of type '${type}'. To indicate empty, deleted, or un-set use null.`; 37 | 38 | case RELATIONSHIP_ERROR_TYPES.INCORRECT_VALUE_TYPE: 39 | return `An unclassified error occurred while validating the relationship '${propertyName}' on a resource of type '${type}'`; 40 | 41 | default: 42 | return `An unclassified error occurred while validating the relationship '${propertyName}' for a resource of type '${type}'`; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /addon/-private/errors/resource-error.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError, createNiceErrorMessage, uniqueErrorId } from './validation-error'; 2 | 3 | import { Value } from 'json-typescript'; 4 | 5 | export const RESOURCE_ERROR_TYPES = { 6 | RESOURCE_MISSING: uniqueErrorId(), 7 | RESOURCE_IS_ARRAY: uniqueErrorId(), 8 | INVALID_RESOURCE: uniqueErrorId(), 9 | UNEXPECTED_KEY: uniqueErrorId(), 10 | MISSING_KEY: uniqueErrorId(), 11 | MISSING_INFO: uniqueErrorId(), 12 | INVALID_ID_VALUE: uniqueErrorId(), 13 | INVALID_TYPE_VALUE: uniqueErrorId(), 14 | INVALID_TYPE_FORMAT: uniqueErrorId(), 15 | UNKNOWN_SCHEMA: uniqueErrorId() 16 | }; 17 | 18 | export class ResourceError extends ValidationError { 19 | constructor(errorType: number, type: string, propertyName: string, value: Value, path: string) { 20 | let errorLocation = ''; 21 | 22 | if ( 23 | errorType !== RESOURCE_ERROR_TYPES.RESOURCE_MISSING && 24 | errorType !== RESOURCE_ERROR_TYPES.RESOURCE_IS_ARRAY && 25 | errorType !== RESOURCE_ERROR_TYPES.MISSING_INFO 26 | ) { 27 | errorLocation = createNiceErrorMessage( 28 | propertyName, 29 | value, 30 | path, 31 | false, 32 | ); 33 | } 34 | const error = buildPrimaryResourceErrorMessage(errorType, type, propertyName || path, value); 35 | const message = error + errorLocation; 36 | super(message); 37 | } 38 | } 39 | 40 | function buildPrimaryResourceErrorMessage(errorType: number, type: string, propertyName: string, value: Value) { 41 | switch (errorType) { 42 | case RESOURCE_ERROR_TYPES.RESOURCE_MISSING: 43 | return `Expected to receive a json-api resource${propertyName ? ' at ' + propertyName : ''} but instead found '${value}'.`; 44 | 45 | case RESOURCE_ERROR_TYPES.RESOURCE_IS_ARRAY: 46 | return `Expected to receive a single json-api resource${propertyName ? ' at ' + propertyName : ''} but instead found an Array.`; 47 | 48 | case RESOURCE_ERROR_TYPES.INVALID_RESOURCE: 49 | return `Expected to receive a json-api resource${propertyName ? ' at ' + propertyName : ''} but instead found '${value}'.`; 50 | 51 | case RESOURCE_ERROR_TYPES.UNEXPECTED_KEY: 52 | return `Unexpected key in payload: ${propertyName}`; 53 | 54 | case RESOURCE_ERROR_TYPES.MISSING_KEY: 55 | return `Missing mandatory key in payload: ${propertyName}`; 56 | 57 | case RESOURCE_ERROR_TYPES.MISSING_INFO: 58 | return `In addition to 'type' and 'id', a resource needs at least one of the following keys: ${value}`; 59 | 60 | case RESOURCE_ERROR_TYPES.INVALID_ID_VALUE: 61 | case RESOURCE_ERROR_TYPES.INVALID_TYPE_VALUE: 62 | return `Resource.${propertyName} must be a string, found ${value}`; 63 | 64 | case RESOURCE_ERROR_TYPES.INVALID_TYPE_FORMAT: 65 | return `Expected resource type to be dasherized, found '${value}' instead of '${type}'.`; 66 | 67 | case RESOURCE_ERROR_TYPES.UNKNOWN_SCHEMA: 68 | return `Unknown resource, no schema was found for type '${value}'`; 69 | 70 | default: 71 | return `An unclassified error occurred while validating the relationship '${propertyName}' for a resource of type '${type}'`; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /addon/-private/errors/validation-error.ts: -------------------------------------------------------------------------------- 1 | import { IErrorOptions } from "ember-data"; 2 | 3 | export class ValidationError extends Error { 4 | constructor(message: string) { 5 | super(message); 6 | 7 | this.name = this.constructor.name; 8 | 9 | if (typeof Error.captureStackTrace === 'function') { 10 | Error.captureStackTrace(this, this.constructor); 11 | } else { 12 | this.stack = (new Error(message)).stack; 13 | } 14 | } 15 | } 16 | 17 | let _ERROR_ID = 0; 18 | 19 | export function uniqueErrorId() { 20 | return _ERROR_ID++; 21 | } 22 | 23 | export const NICE_ERROR_TYPES = { 24 | KEY_ERROR: 1, 25 | VALUE_ERROR: 2, 26 | OBJECT_ERROR: 3, 27 | }; 28 | 29 | export function createNiceErrorMessage(options: IErrorOptions) { 30 | let { key, value, path, code } = options; 31 | let parts, message, depth; 32 | 33 | switch (code) { 34 | case NICE_ERROR_TYPES.KEY_ERROR: 35 | parts = path.split('.').filter(i => i !== ''); 36 | message = "\n\n\t{\n\t"; 37 | depth = 2; 38 | 39 | for (let i = 0; i < parts.length; i++) { 40 | message += `${padLeft(parts[i], depth)}: {\n\t`; 41 | depth += 2; 42 | } 43 | 44 | message += `${padLeft(key, depth)}: ${typeof value === 'string' ? "'" + value + "'" : value}\n\t`; 45 | message += `${padLeft('^', depth, '-')}\n\n`; 46 | 47 | return message; 48 | 49 | case NICE_ERROR_TYPES.OBJECT_ERROR: 50 | parts = path.split('.').filter(i => i !== ''); 51 | message = "\n\n\t" + String(value) + "\n"; 52 | message += `${padLeft('^', 3, '-')}\n\n`; 53 | return message; 54 | 55 | case NICE_ERROR_TYPES.VALUE_ERROR: 56 | parts = path.split('.').filter(i => i !== ''); 57 | message = "\n\n\t{\n\t"; 58 | depth = 2; 59 | 60 | for (let i = 0; i < parts.length; i++) { 61 | message += `${padLeft(parts[i], depth)}: {\n\t`; 62 | depth += 2; 63 | } 64 | 65 | message += `${padLeft(key, depth)}: ${typeof value === 'string' ? "'" + value + "'" : value}\n\t`; 66 | depth += key.length + 2; 67 | message += `${padLeft('^', depth, '-')}\n\n`; 68 | 69 | return message; 70 | 71 | default: 72 | throw new Error('Cannot format error for unknown error code'); 73 | } 74 | } 75 | 76 | function padLeft(str: string, count = 0, char = ' ') { 77 | let s = ''; 78 | 79 | while (count--) { 80 | s += char; 81 | } 82 | 83 | return s + str; 84 | } 85 | -------------------------------------------------------------------------------- /addon/-private/meta-rules/it-has-required-sibling.ts: -------------------------------------------------------------------------------- 1 | import { MetaError, META_ERROR_TYPES } from '../errors/meta-error'; 2 | import memberPresent from '../utils/member-present'; 3 | import memberDefinedAndNotNull from '../utils/member-defined-and-not-null'; 4 | 5 | import { IValidationContext } from 'ember-data'; 6 | import { DocWithMeta } from 'jsonapi-typescript'; 7 | 8 | /** 9 | * Validates that a document has data or errors in addition to meta 10 | * 11 | * @param validator 12 | * @param document 13 | * @param target 14 | * @param requiredSiblings 15 | * @param issues 16 | * @param path 17 | * @returns {boolean} 18 | */ 19 | export default function itHasRequiredSibling({ 20 | validator, 21 | document, 22 | target, 23 | requiredSiblings, 24 | issues, 25 | path, 26 | }: IValidationContext) { 27 | let { errors } = issues; 28 | 29 | if (!memberPresent(target, 'meta')) { 30 | return true; 31 | } 32 | 33 | for (let i = 0; i < requiredSiblings.length; i++) { 34 | if (memberDefinedAndNotNull(target, requiredSiblings[i])) { 35 | return true; 36 | } 37 | } 38 | 39 | let error = new MetaError({ 40 | document, 41 | path, 42 | target, 43 | member: 'meta', 44 | code: META_ERROR_TYPES.DISALLOWED_SOLITARY_META_MEMBER, 45 | validator, 46 | value: (target as DocWithMeta).meta, 47 | expectedValue: requiredSiblings 48 | }); 49 | 50 | errors.push(error); 51 | 52 | return false; 53 | } 54 | -------------------------------------------------------------------------------- /addon/-private/utils/about-an-oxford-comma.ts: -------------------------------------------------------------------------------- 1 | export default function aboutAnOxfordComma(array: string[], quote = '`', joinWord = 'or') { 2 | let arr = array.slice(); 3 | 4 | if (arr.length === 0) { 5 | throw new Error('No items to list from'); 6 | } 7 | 8 | if (arr.length === 1) { 9 | return `${quote}${arr[0]}${quote}`; 10 | } 11 | 12 | let last = arr.pop(); 13 | 14 | return quote + arr.join(quote + ', ' + quote) + quote + ' ' + joinWord + ' ' + quote + last + quote; 15 | } 16 | -------------------------------------------------------------------------------- /addon/-private/utils/assert-member-format.ts: -------------------------------------------------------------------------------- 1 | import isDasherized from './is-dasherized'; 2 | 3 | export default function assertMemberFormat(type: string, shouldDasherize = false) { 4 | let errors = []; 5 | 6 | if (shouldDasherize) { 7 | if (!isDasherized(type)) { 8 | errors.push('dasherize'); 9 | } 10 | } else { 11 | if (isDasherized(type)) { 12 | errors.push('whoops, dasherized'); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /addon/-private/utils/assert-type-format.ts: -------------------------------------------------------------------------------- 1 | import isDasherized from './is-dasherized'; 2 | import isNormalizedType from './is-normalized-type'; 3 | 4 | export default function assertTypeFormat(type: any, formatter = isNormalizedType, shouldDasherize = true) { 5 | let formattedType = formatter(type); 6 | let errors = []; 7 | 8 | // TODO: always returns true, since string (type) and boolean are never equivelant 9 | if (type !== formattedType) { 10 | errors.push('yes'); 11 | } 12 | 13 | if (shouldDasherize) { 14 | if (!isDasherized(type)) { 15 | errors.push('dasherize'); 16 | } 17 | } else { 18 | if (isDasherized(type)) { 19 | errors.push('whoops, dasherized'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /addon/-private/utils/is-camel.ts: -------------------------------------------------------------------------------- 1 | import { camelize } from '@ember/string'; 2 | 3 | export default function isCamel(str: string) { 4 | let camelized = camelize(str); 5 | 6 | return camelized === str; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/utils/is-dasherized.ts: -------------------------------------------------------------------------------- 1 | import { dasherize } from '@ember/string'; 2 | 3 | export default function isDasherized(str: string) { 4 | let dasherized = dasherize(str); 5 | 6 | return dasherized === str; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/utils/is-normalized-type.ts: -------------------------------------------------------------------------------- 1 | import normalizeType from './normalize-type'; 2 | 3 | export default function isNormalized(type: string) { 4 | let normalized = normalizeType(type); 5 | 6 | return normalized === type; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/utils/is-plain-object.ts: -------------------------------------------------------------------------------- 1 | export default function isPlainObject(obj: object) { 2 | return typeof obj === 'object' 3 | && obj !== null 4 | && ( 5 | obj.constructor === Object || // {} 6 | obj.constructor === undefined // Object.create(null) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /addon/-private/utils/member-defined-and-not-null.ts: -------------------------------------------------------------------------------- 1 | import memberDefined from './member-defined'; 2 | 3 | import { IObject } from 'ember-data'; 4 | 5 | export default function memberDefinedAndNotNull(obj: IObject, member: string) { 6 | return memberDefined(obj, member) && obj[member] !== null; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/utils/member-defined.ts: -------------------------------------------------------------------------------- 1 | import memberPresent from './member-present'; 2 | 3 | import { IObject } from 'ember-data'; 4 | 5 | export default function memberDefined(obj: IObject, member: string) { 6 | return memberPresent(obj, member) && obj[member] !== undefined; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/utils/member-present.ts: -------------------------------------------------------------------------------- 1 | export default function memberPresent(obj: object, member: string) { 2 | return Object.prototype.hasOwnProperty.call(obj, member); 3 | } 4 | -------------------------------------------------------------------------------- /addon/-private/utils/normalize-type.ts: -------------------------------------------------------------------------------- 1 | import { singularize } from 'ember-inflector'; 2 | import { dasherize } from '@ember/string'; 3 | 4 | export default function normalizeType(str: string) { 5 | return singularize(dasherize(str)); 6 | } 7 | -------------------------------------------------------------------------------- /addon/-private/utils/type-of.ts: -------------------------------------------------------------------------------- 1 | type PossibleTypes = 2 | | "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function" 3 | | 'Array' 4 | | 'Date' 5 | | 'Null'; 6 | 7 | // TODO: use guardclauses instead of re-assigning the value of type 8 | // so that the type usage becomes simpler 9 | export default function typeOf(value: any): string { 10 | let type: PossibleTypes = typeof value; 11 | 12 | if (type === "object") { 13 | if (value instanceof Array) { 14 | type = 'Array'; 15 | } else if (value instanceof Date) { 16 | type = "Date"; 17 | } else if (value === null) { 18 | type = "Null"; 19 | } else { 20 | type = value; 21 | } 22 | } 23 | 24 | return type; 25 | } 26 | -------------------------------------------------------------------------------- /addon/-private/validate-attributes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AttributeError, 3 | ATTRIBUTE_ERROR_TYPES 4 | } from "./errors/attribute-error"; 5 | 6 | export default function validateResourceAttributes(contextObject) { 7 | let { 8 | schema, 9 | attributes, 10 | /*methodName,*/ 11 | path, 12 | validator } = contextObject; 13 | 14 | if (typeof attributes !== "object" || attributes === null) { 15 | return [ 16 | new Error( 17 | `Expected the attributes hash for a resource to be an object, found '${attributes}'` 18 | ) 19 | ]; 20 | } 21 | 22 | let foundRelationshipKeys = Object.keys(attributes); 23 | let errors = []; 24 | 25 | for (let i = 0; i < foundRelationshipKeys.length; i++) { 26 | let key = foundRelationshipKeys[i]; 27 | let data = attributes[key]; 28 | let attr = findAttribute({ schema, key, validator }); 29 | 30 | if (attr === undefined) { 31 | errors.push( 32 | new AttributeError( 33 | ATTRIBUTE_ERROR_TYPES.UNKNOWN_ATTRIBUTE, 34 | schema.type, 35 | key, 36 | data, 37 | path 38 | ) 39 | ); 40 | } 41 | 42 | if (typeof data === "undefined") { 43 | errors.push( 44 | new AttributeError( 45 | ATTRIBUTE_ERROR_TYPES.UNDEFINED_VALUE, 46 | schema.type, 47 | key, 48 | data, 49 | path 50 | ) 51 | ); 52 | } 53 | } 54 | 55 | return errors; 56 | } 57 | 58 | export function findAttribute({ schema, key: propertyName, validator }) { 59 | let arr = schema.attr; 60 | 61 | if (arr) { 62 | for (let i = 0; i < arr.length; i++) { 63 | if (arr[i] === propertyName) { 64 | let meta = {}; 65 | meta.key = propertyName; 66 | meta.kind = "attribute"; 67 | meta.for = schema.type; 68 | return meta; 69 | } 70 | } 71 | } 72 | 73 | if (schema.inherits) { 74 | return findAttribute({ 75 | schema: validator.schemaFor(schema.inherits), 76 | key: propertyName, 77 | validator 78 | }); 79 | } 80 | 81 | return undefined; 82 | } 83 | -------------------------------------------------------------------------------- /addon/-private/validate-document.ts: -------------------------------------------------------------------------------- 1 | import itExists from './document-rules/it-exists'; 2 | import itHasAtLeastOne from './document-rules/it-has-at-least-one'; 3 | import itHasAtLeastOneNonNull from './document-rules/it-has-at-least-one-non-null'; 4 | import itCantHaveBoth from './document-rules/it-cant-have-both'; 5 | import itHasNoUnknownMembers from './document-rules/it-has-no-unknown-members'; 6 | import includedMustHaveData from './document-rules/included-must-have-data'; 7 | import validateJsonapiMember from './validate-jsonapi-member'; 8 | import validateMeta from './validate-meta'; 9 | import dataIsValid from './document-rules/data-is-valid'; 10 | import includedIsValid from './document-rules/included-is-valid'; 11 | import validateLinks from './validate-links'; 12 | // import itHasValidErrors from './document-rules/it-has-valid-errors'; 13 | 14 | import JSONAPIValidator from '@ember-data/json-api-validator/-private/validator'; 15 | import { Document } from 'jsonapi-typescript'; 16 | import { IValidationContext } from 'ember-data'; 17 | 18 | 19 | interface IValidateDocument { 20 | validator: JSONAPIValidator; 21 | document: Document; 22 | issues?: unknown; 23 | path?: string; 24 | } 25 | 26 | /** 27 | * Validate that a json-api document conforms to spec 28 | * 29 | * Spec: http://jsonapi.org/format/#document-top-level 30 | * 31 | * @param validator 32 | * @param document 33 | * @param {Array} issues 34 | * @param {String} path 35 | * 36 | * @returns {Object} an object with arrays of `errors` and `warnings`. 37 | */ 38 | export default function _validateDocument({ 39 | validator, 40 | document, 41 | issues, 42 | path = '', 43 | }: IValidateDocument) { 44 | issues = issues || { 45 | errors: [], 46 | warnings: [], 47 | }; 48 | 49 | let validationContext: IValidationContext = { 50 | validator, 51 | document, 52 | target: document, 53 | issues, 54 | path, 55 | }; 56 | 57 | if (itExists(validationContext)) { 58 | itHasAtLeastOne(validationContext); 59 | itHasAtLeastOneNonNull(validationContext); 60 | itCantHaveBoth(validationContext); 61 | itHasNoUnknownMembers(validationContext); 62 | includedMustHaveData(validationContext); 63 | validateJsonapiMember(validationContext); 64 | validateMeta(validationContext); 65 | dataIsValid(validationContext); 66 | includedIsValid(validationContext); 67 | validateLinks(validationContext); 68 | 69 | // TODO validate errors 70 | // itHasValidErrors(validationContext); 71 | } 72 | 73 | return issues; 74 | } 75 | -------------------------------------------------------------------------------- /addon/-private/validate-jsonapi-member.ts: -------------------------------------------------------------------------------- 1 | import { DocumentError, DOCUMENT_ERROR_TYPES } from './errors/document-error'; 2 | import validateMeta from './validate-meta'; 3 | import memberPresent from './utils/member-present'; 4 | import memberDefinedAndNotNull from './utils/member-defined-and-not-null'; 5 | 6 | /** 7 | * 8 | * @param validator 9 | * @param document 10 | * @param issues 11 | * @param path 12 | * @returns {boolean} 13 | */ 14 | export default function validateJsonapiMember({ 15 | validator, 16 | document, 17 | issues, 18 | path, 19 | }) { 20 | let { errors } = issues; 21 | let hasError = false; 22 | 23 | if (memberPresent(document, 'jsonapi')) { 24 | if (!memberDefinedAndNotNull(document, 'jsonapi')) { 25 | errors.push( 26 | new DocumentError({ 27 | code: DOCUMENT_ERROR_TYPES.VALUE_MUST_BE_OBJECT, 28 | value: document.jsonapi, 29 | member: 'jsonapi', 30 | path, 31 | document, 32 | validator, 33 | }) 34 | ); 35 | } else { 36 | let keys = Object.keys(document.jsonapi); 37 | 38 | /* 39 | The spec allows this to be empty, but we are more strict. If the jsonapi 40 | property is defined we expect it to have information. 41 | */ 42 | if (keys.length === 0 || !memberPresent(document.jsonapi, 'version')) { 43 | errors.push( 44 | new DocumentError({ 45 | code: DOCUMENT_ERROR_TYPES.MISSING_VERSION, 46 | value: document.jsonapi, 47 | member: 'jsonapi', 48 | path, 49 | document, 50 | validator, 51 | }) 52 | ); 53 | hasError = true; 54 | } else { 55 | /* 56 | The spec only allows for 'version' and 'meta'. 57 | */ 58 | for (let i = 0; i < keys.length; i++) { 59 | let key = keys[i]; 60 | 61 | if (key === 'version') { 62 | if ( 63 | typeof document.jsonapi.version !== 'string' || 64 | document.jsonapi.version.length === 0 65 | ) { 66 | errors.push( 67 | new DocumentError({ 68 | code: DOCUMENT_ERROR_TYPES.VERSION_MUST_BE_STRING, 69 | value: document.jsonapi.version, 70 | member: 'jsonapi', 71 | path, 72 | document, 73 | validator, 74 | }) 75 | ); 76 | hasError = true; 77 | } 78 | } else if (key === 'meta') { 79 | hasError = 80 | !validateMeta({ 81 | validator, 82 | target: document.jsonapi, 83 | document, 84 | issues, 85 | path: path + '.jsonapi', 86 | }) || hasError; 87 | } else { 88 | errors.push( 89 | new DocumentError({ 90 | code: DOCUMENT_ERROR_TYPES.UNKNOWN_MEMBER, 91 | value: document.jsonapi.version, 92 | member: 'jsonapi', 93 | path, 94 | document, 95 | validator, 96 | }) 97 | ); 98 | hasError = true; 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | return !hasError; 106 | } 107 | -------------------------------------------------------------------------------- /addon/-private/validate-links.ts: -------------------------------------------------------------------------------- 1 | import { LinksError, LINKS_ERROR_TYPES} from './errors/links-error'; 2 | import memberPresent from './utils/member-present'; 3 | import isPlainObject from './utils/is-plain-object'; 4 | 5 | /** 6 | * `links` MUST be an object if present 7 | * each member MUST be a string URL or a link-object 8 | * each link-object MUST have an `href` string URL 9 | * and MAY have a meta object. 10 | * 11 | * For top level documents (here), the links-object MAY contain 12 | * 13 | * - self: the link to the current document 14 | * - related: when the data represents a resource relationship 15 | * - pagination links: for the primary data 16 | * 17 | * The following keys MUST be used for pagination links: 18 | * 19 | * first: the first page of data 20 | * last: the last page of data 21 | * prev: the previous page of data 22 | * next: the next page of data 23 | * 24 | * Keys MUST either be omitted or have a `null` value to indicate that a particular link is unavailable. 25 | * 26 | * @param document 27 | * @param validator 28 | * @param target 29 | * @param issues 30 | * @param path 31 | * @returns {boolean} 32 | */ 33 | export default function validateLinks({ document, validator, target, issues, path }) { 34 | let { errors } = issues; 35 | 36 | if (memberPresent(target, 'links')) { 37 | if (!isPlainObject(target.links)) { 38 | errors.push(new LinksError({ 39 | code: LINKS_ERROR_TYPES.VALUE_MUST_BE_OBJECT, 40 | value: target.links, 41 | member: 'links', 42 | target, 43 | validator, 44 | document, 45 | path 46 | })); 47 | 48 | return false; 49 | } else if (Object.keys(target.links).length === 0) { 50 | errors.push( 51 | new LinksError({ 52 | code: LINKS_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY, 53 | value: target.links, 54 | member: 'links', 55 | target, 56 | validator, 57 | document, 58 | path 59 | }) 60 | ); 61 | 62 | return false; 63 | } 64 | } 65 | return true; 66 | } 67 | -------------------------------------------------------------------------------- /addon/-private/validate-meta.ts: -------------------------------------------------------------------------------- 1 | import { MetaError, META_ERROR_TYPES } from './errors/meta-error'; 2 | import memberPresent from './utils/member-present'; 3 | import isPlainObject from './utils/is-plain-object'; 4 | import itHasRequiredSibling from './meta-rules/it-has-required-sibling'; 5 | 6 | /** 7 | * 8 | * @param validator 9 | * @param isRelationship 10 | * @param document 11 | * @param target 12 | * @param issues 13 | * @param path 14 | * @returns {boolean} 15 | */ 16 | export default function validateObjectMeta({ 17 | validator, 18 | isRelationship = false, 19 | document, 20 | target, 21 | issues, 22 | path, 23 | }) { 24 | let { errors } = issues; 25 | 26 | if (memberPresent(target, 'meta')) { 27 | let hasError = false; 28 | 29 | if (target === document && validator.disallowMetaOnlyDocuments()) { 30 | hasError = itHasRequiredSibling({ 31 | validator, 32 | document, 33 | target, 34 | issues, 35 | path, 36 | requiredSiblings: ['data', 'errors'], 37 | }); 38 | } else if (isRelationship && validator.disallowMetaOnlyRelationships()) { 39 | hasError = itHasRequiredSibling({ 40 | validator, 41 | document, 42 | target, 43 | issues, 44 | path, 45 | requiredSiblings: ['data', 'links'], 46 | }); 47 | } 48 | 49 | if (!isPlainObject(target.meta)) { 50 | errors.push( 51 | new MetaError({ 52 | code: META_ERROR_TYPES.VALUE_MUST_BE_OBJECT, 53 | value: target.meta, 54 | target, 55 | member: 'meta', 56 | validator, 57 | document, 58 | path, 59 | }) 60 | ); 61 | 62 | return false; 63 | } else if (!Object.keys(target.meta).length > 0) { 64 | errors.push( 65 | new MetaError({ 66 | code: META_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY, 67 | value: target.meta, 68 | target, 69 | member: 'meta', 70 | validator, 71 | document, 72 | path, 73 | }) 74 | ); 75 | 76 | return false; 77 | } 78 | 79 | return hasError; 80 | } 81 | 82 | return true; 83 | } 84 | -------------------------------------------------------------------------------- /addon/-private/validate-reference.ts: -------------------------------------------------------------------------------- 1 | import { REFERENCE_ERROR_TYPES, ReferenceError } from './errors/reference-error'; 2 | import { dasherize } from 'json-api-validations/-private/utils/dasherize'; 3 | 4 | const ALL_REFERENCE_KEYS = ['type', 'id', 'meta']; 5 | 6 | function isObject(obj) { 7 | return typeof obj === 'object' && obj !== null; 8 | } 9 | 10 | /** 11 | * 12 | * @param reference 13 | * @param errors 14 | * @param path 15 | * @returns {boolean} 16 | */ 17 | export default function validateReference(reference, errors, path = '') { 18 | if (!isObject(reference)) { 19 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.INVALID_REFERENCE, undefined, undefined, reference, path)); 20 | return false; 21 | } 22 | 23 | let hasError = false; 24 | let keys = Object.keys(reference); 25 | for (let i = 0; i < keys.length; i++) { 26 | let key = keys[i]; 27 | 28 | if (ALL_REFERENCE_KEYS.indexOf(key) === -1) { 29 | hasError = true; 30 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.UNEXPECTED_KEY, reference.type, key, reference, path)); 31 | } 32 | } 33 | 34 | if (typeof reference.id !== 'string' || reference.id.length === 0) { 35 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.INVALID_ID_VALUE, undefined, 'id', reference.id, path)); 36 | } 37 | if (typeof reference.type !== 'string' || reference.type.length === 0) { 38 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.INVALID_TYPE_VALUE, undefined, 'type', reference.type, path)); 39 | } else { 40 | let dasherized = dasherize(reference.type); 41 | 42 | if (dasherized !== reference.type) { 43 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.INVALID_TYPE_FORMAT, dasherized, 'type', reference.type, path)); 44 | } 45 | 46 | let schema = this.schemaFor(dasherized); 47 | 48 | if (schema === undefined) { 49 | errors.push(new ReferenceError(REFERENCE_ERROR_TYPES.UNKNOWN_SCHEMA, reference.type, 'type', reference.type, path)); 50 | hasError = true; 51 | } 52 | } 53 | 54 | hasError = !this.validateMeta(reference, errors, path) || hasError; 55 | 56 | return !hasError; 57 | } 58 | -------------------------------------------------------------------------------- /addon/-private/validate-relationships.ts: -------------------------------------------------------------------------------- 1 | import { dasherize } from "@ember/string"; 2 | import { 3 | RELATIONSHIP_ERROR_TYPES, 4 | RelationshipError 5 | } from "./errors/relationship-error"; 6 | 7 | export default function validateResourceRelationships(contextObject) { 8 | let { 9 | schema, 10 | relationships, 11 | /*methodName,*/ 12 | path = "", 13 | validator 14 | } = contextObject; 15 | 16 | if (typeof relationships !== "object" || relationships === null) { 17 | let error = new RelationshipError( 18 | RELATIONSHIP_ERROR_TYPES.INVALID_HASH, 19 | schema.type, 20 | "relationships", 21 | relationships, 22 | path 23 | ); 24 | 25 | return [error]; 26 | } 27 | 28 | let foundRelationshipKeys = Object.keys(relationships); 29 | let errors = []; 30 | 31 | for (let i = 0; i < foundRelationshipKeys.length; i++) { 32 | let key = foundRelationshipKeys[i]; 33 | let data = relationships[key]; 34 | errors = errors.concat( 35 | validateResourceRelationship({ schema, key, data, path, validator }) 36 | ); 37 | } 38 | 39 | return errors; 40 | } 41 | 42 | function validateResourceRelationship({ 43 | schema, 44 | key: propertyName, 45 | data, 46 | path, 47 | validator 48 | }) { 49 | let relationship = _findRelationship({ schema, propertyName, validator }); 50 | let errors = []; 51 | 52 | if (relationship === undefined) { 53 | errors.push( 54 | new RelationshipError( 55 | RELATIONSHIP_ERROR_TYPES.UNKNOWN_RELATIONSHIP, 56 | schema.type, 57 | propertyName, 58 | data, 59 | path 60 | ) 61 | ); 62 | } else { 63 | if (typeof data !== "object") { 64 | errors.push( 65 | new Error( 66 | `The data for ${propertyName} on ${schema.type} is not an object` 67 | ) 68 | ); 69 | } else if (data !== null) { 70 | if (data.hasOwnProperty("links")) { 71 | if (typeof data.links !== "object") { 72 | errors.push( 73 | new Error( 74 | `The links for the relationship ${propertyName} on ${ 75 | schema.type 76 | } is not an object or null` 77 | ) 78 | ); 79 | } 80 | } 81 | if (data.hasOwnProperty("meta")) { 82 | if (typeof data.meta !== "object") { 83 | errors.push( 84 | new Error( 85 | `The meta for the relationship ${propertyName} on ${ 86 | schema.type 87 | } is not an object or null` 88 | ) 89 | ); 90 | } 91 | } 92 | if (data.hasOwnProperty("data")) { 93 | if (typeof data.data !== "object") { 94 | errors.push( 95 | new Error( 96 | `The data for the relationship ${propertyName} on ${ 97 | schema.type 98 | } is not an object or null` 99 | ) 100 | ); 101 | } else if (data.data !== null) { 102 | if (relationship.kind === "hasMany") { 103 | if (!Array.isArray(data.data)) { 104 | errors.push( 105 | new Error( 106 | `The data for the hasMany relationship ${propertyName} on ${ 107 | schema.type 108 | } is not an array` 109 | ) 110 | ); 111 | } else { 112 | data.data.forEach(ref => { 113 | errors = errors.concat( 114 | validateRelationshipReference({ 115 | relationship, 116 | ref, 117 | validator 118 | }) 119 | ); 120 | }); 121 | } 122 | } else { 123 | // TODO belongsTo support 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | return errors; 131 | } 132 | 133 | function validateRelationshipReference({ relationship, ref, validator }) { 134 | let errors = []; 135 | 136 | if (typeof ref.id !== "string" || ref.id.length === 0) { 137 | errors.push(new Error(`Missing id on ref`)); 138 | } 139 | 140 | if (typeof ref.type !== "string" || ref.type.length === 0) { 141 | errors.push(new Error(`Missing type on ref`)); 142 | } else { 143 | let foundType = dasherize(ref.type); 144 | 145 | if (foundType !== ref.type) { 146 | errors.push(new Error(`type should be dasherized`)); 147 | } 148 | 149 | let type = relationship.schema; 150 | let schema = validator.schemaFor(type); 151 | let isCorrectType = foundType === type; 152 | 153 | while (!isCorrectType && schema.inherits) { 154 | schema = validator.schemaFor(schema.inherits); 155 | isCorrectType = foundType === schema.type; 156 | } 157 | 158 | if (!isCorrectType) { 159 | errors.push( 160 | new Error(`reference type is not valid for this relationship`) 161 | ); 162 | } 163 | } 164 | 165 | return errors; 166 | } 167 | 168 | function _findRelationship({ schema, propertyName, validator }) { 169 | let relTypes = ["hasMany", "belongsTo"]; 170 | 171 | for (let i = 0; i < relTypes.length; i++) { 172 | let kind = relTypes[i]; 173 | let arr = schema[kind]; 174 | 175 | if (arr) { 176 | for (let i = 0; i < arr.length; i++) { 177 | if (arr[i].key === propertyName) { 178 | let meta = arr[i]; 179 | meta.kind = kind; 180 | meta.for = schema.type; 181 | return meta; 182 | } 183 | } 184 | } 185 | } 186 | 187 | if (schema.inherits) { 188 | return _findRelationship({ 189 | schema: validator.schemaFor(schema.inherits), 190 | propertyName, 191 | validator 192 | }); 193 | } 194 | 195 | return undefined; 196 | } 197 | -------------------------------------------------------------------------------- /addon/-private/validate-resource.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { dasherize } from '@ember/string'; 3 | import validateResourceRelationships from './validate-relationships'; 4 | import validateResourceAttributes from './validate-attributes'; 5 | import { RESOURCE_ERROR_TYPES, ResourceError } from './errors/resource-error'; 6 | 7 | const _EXPECTED_RESOURCE_KEYS = [ 8 | 'id', 'type', 'attributes', 'relationships','links', 'meta' 9 | ]; 10 | const _MANDATORY_PRIMARY_RESOURCE_KEYS = [ 11 | 'id', 'type' 12 | ]; 13 | const _MANDATORY_SECONDARY_RESOURCE_KEYS = [ 14 | 'attributes', 'relationships' 15 | ]; 16 | */ 17 | 18 | /** 19 | * 20 | * @param validator 21 | * @param document 22 | * @param issues 23 | * @param target 24 | * @param path 25 | */ 26 | export default function validateResource(/*contextObject*/) { 27 | // TODO don't early return; 28 | return true; 29 | 30 | // let { validator, document, issues, target, path } = contextObject; 31 | /* 32 | if (!resource) { 33 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.RESOURCE_MISSING, undefined, undefined, resource, path)); 34 | 35 | } else if (Array.isArray(resource)) { 36 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.RESOURCE_IS_ARRAY, undefined, undefined, resource, path)); 37 | 38 | } else if (typeof resource !== 'object') { 39 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.INVALID_RESOURCE, undefined, undefined, resource, path)); 40 | 41 | } else { 42 | errors = errors.concat(detectStructuralErrors(Object.assign({resource, methodName}, contextObject))); 43 | errors = errors.concat(detectTypeErrors(Object.assign({resource, methodName}, contextObject))); 44 | } 45 | */ 46 | } 47 | 48 | /* 49 | function detectStructuralErrors(contextObject) { 50 | let {resource: payload, methodName, path} = contextObject; 51 | let resourceKeys = Object.keys(payload); 52 | let errors = []; 53 | 54 | for (let i = 0; i < resourceKeys.length; i++) { 55 | let key = resourceKeys[i]; 56 | 57 | if (_EXPECTED_RESOURCE_KEYS.indexOf(key) === -1) { 58 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.UNEXPECTED_KEY, undefined, key, payload[key], path)); 59 | } 60 | } 61 | 62 | for (let i = 0; i < _MANDATORY_PRIMARY_RESOURCE_KEYS.length; i++) { 63 | let key = _MANDATORY_PRIMARY_RESOURCE_KEYS[i]; 64 | 65 | if (resourceKeys.indexOf(key) === -1) { 66 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.MISSING_KEY, undefined, key, payload, path)); 67 | } 68 | } 69 | 70 | let foundSecondaryKey = false; 71 | for (let i = 0; i < _MANDATORY_SECONDARY_RESOURCE_KEYS.length; i++) { 72 | let key = _MANDATORY_SECONDARY_RESOURCE_KEYS[i]; 73 | 74 | if (resourceKeys.indexOf(key) !== -1) { 75 | foundSecondaryKey = true; 76 | break; 77 | } 78 | } 79 | 80 | if (foundSecondaryKey === false) { 81 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.MISSING_INFO, payload.type, undefined, _MANDATORY_SECONDARY_RESOURCE_KEYS.join(', '), path)); 82 | } 83 | 84 | return errors; 85 | } 86 | 87 | function detectTypeErrors(contextObject) { 88 | let {resource, methodName, path, validator} = contextObject; 89 | let schema; 90 | let errors = []; 91 | 92 | if (typeof resource.id !== 'string' || resource.id.length === 0) { 93 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.INVALID_ID_VALUE, undefined, 'id', resource.id, path)); 94 | } 95 | if (typeof resource.type !== 'string' || resource.type.length === 0) { 96 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.INVALID_TYPE_VALUE, undefined, 'type', resource.type, path)); 97 | 98 | } else { 99 | let dasherized = dasherize(resource.type); 100 | 101 | if (dasherized !== resource.type) { 102 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.INVALID_TYPE_FORMAT, dasherized, 'type', resource.type, path)); 103 | } 104 | 105 | schema = validator.schemaFor(dasherized); 106 | 107 | if (schema === undefined) { 108 | errors.push(new ResourceError(RESOURCE_ERROR_TYPES.UNKNOWN_SCHEMA, resource.type, 'type', resource.type, path)); 109 | } else { 110 | if (resource.hasOwnProperty('attributes')) { 111 | errors = errors.concat(validateResourceAttributes(Object.assign({schema, resource.attributes}, contextObject)})); 112 | } 113 | if (resource.hasOwnProperty('relationships')) { 114 | errors = errors.concat(validateResourceRelationships(Object.assign({schema, resource.relationships}, contextObject))); 115 | } 116 | } 117 | } 118 | 119 | return errors; 120 | } 121 | */ 122 | -------------------------------------------------------------------------------- /addon/-private/validator.ts: -------------------------------------------------------------------------------- 1 | import _validateDocument from './validate-document'; 2 | import coalesceAndThrowErrors from './coalesce-errors'; 3 | import assertTypeFormat from './utils/assert-type-format'; 4 | import assertMemberFormat from './utils/assert-member-format'; 5 | import normalizeType from './utils/normalize-type'; 6 | import { dasherize } from '@ember/string'; 7 | 8 | import { Document } from 'jsonapi-typescript'; 9 | import { ISchema } from 'ember-data'; 10 | 11 | 12 | 13 | export interface IJSONAPIValidatorOptions { 14 | strictMode?: boolean; 15 | schemaFor: (type: string) => ISchema | undefined; 16 | schemaImplements: (subclassType: string, type: string) => boolean; 17 | formatFallbackType?: (value: string) => string; 18 | disallowMetaOnlyDocuments?: () => boolean; 19 | disallowMetaOnlyRelationships?: () => boolean; 20 | assertTypeFormat?: unknown; 21 | assertMemberFormat?: unknown; 22 | formatType?: unknown; 23 | } 24 | 25 | export default class JSONAPIValidator implements IJSONAPIValidatorOptions { 26 | strictMode?: boolean | undefined; 27 | schemaFor: (type: string) => ISchema | undefined; 28 | schemaImplements: (subclassType: string, type: string) => boolean; 29 | formatFallbackType?: ((value: string) => string) | undefined; 30 | disallowMetaOnlyDocuments?: (() => boolean) | undefined; 31 | disallowMetaOnlyRelationships?: (() => boolean) | undefined; 32 | assertTypeFormat?: unknown; 33 | assertMemberFormat?: unknown; 34 | formatType?: unknown; 35 | 36 | constructor(hooks: IJSONAPIValidatorOptions) { 37 | /** 38 | * when strictMode is disabled, the following "innocuous" 39 | * errors become warnings. 40 | * 41 | * # Documents 42 | * 43 | * - empty `included` 44 | * - unknown members 45 | * 46 | * The following mistakes will still error 47 | * 48 | * # Documents 49 | * 50 | * - payloads that have no entries in `data` nor `included` 51 | * 52 | * @type {boolean} default `true` 53 | */ 54 | this.strictMode = !!hooks.strictMode || true; 55 | this.schemaFor = hooks.schemaFor; 56 | this.schemaImplements = hooks.schemaImplements; 57 | 58 | // used to check for a schema by a slightly different name to be friendly 59 | this.formatFallbackType = hooks.formatFallbackType || dasherize; 60 | 61 | this.disallowMetaOnlyDocuments = hooks.disallowMetaOnlyDocuments || function() { return true }; 62 | this.disallowMetaOnlyRelationships = hooks.disallowMetaOnlyRelationships || function() { return true }; 63 | 64 | /* 65 | Ember Data strictly requires singularized, dasherized types 66 | */ 67 | this.assertTypeFormat = hooks.assertTypeFormat || assertTypeFormat; 68 | this.assertMemberFormat = hooks.assertMemberFormat || assertMemberFormat; 69 | this.formatType = hooks.formatType || normalizeType; 70 | } 71 | 72 | /** 73 | * Validate that a json-api document conforms to spec 74 | * 75 | * Spec: http://jsonapi.org/format/#document-top-level 76 | * 77 | * @param document 78 | * @returns {Array} 79 | */ 80 | validateDocument(document: Document) { 81 | let issues = _validateDocument({ validator: this, document }); 82 | 83 | coalesceAndThrowErrors(issues); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /addon/setup-ember-data-validations.ts: -------------------------------------------------------------------------------- 1 | import DS, { ISchema } from 'ember-data'; 2 | import Validator from './-private/validator'; 3 | import { Document } from 'jsonapi-typescript'; 4 | 5 | const { Store } = DS; 6 | 7 | export default function setupEmberDataValidations(_Store = Store as any) { 8 | _Store.reopen({ 9 | init() { 10 | this._super(); 11 | let store = this; 12 | 13 | this.__validator = new Validator({ 14 | schemaImplements(subclassType, type) { 15 | try { 16 | let a = store.modelFor(type); 17 | let b = store.modelFor(subclassType); 18 | 19 | return a.detect(b); 20 | } catch (e) { 21 | return false; 22 | } 23 | }, 24 | schemaFor(type) { 25 | let modelClass; 26 | 27 | try { 28 | modelClass = store.modelFor(type); 29 | } catch (e) { 30 | return undefined; 31 | } 32 | 33 | const schema: ISchema = { 34 | 35 | }; 36 | modelClass.eachRelationship((name, meta) => { 37 | let kind = meta.kind; 38 | schema[kind] = schema[kind] || []; 39 | schema[kind].push({ 40 | key: name, 41 | schema: meta.type, 42 | }); 43 | }); 44 | modelClass.eachAttribute((name /*, meta*/) => { 45 | schema.attr = schema.attr || []; 46 | schema.attr.push(name); 47 | }); 48 | 49 | return schema; 50 | } 51 | }); 52 | }, 53 | 54 | validateJsonApiDocument(jsonApiDocument: Document) { 55 | this.__validator.validateDocument(jsonApiDocument); 56 | }, 57 | 58 | _push(jsonApiDocument: Document) { 59 | this.validateJsonApiDocument(jsonApiDocument); 60 | return this._super(jsonApiDocument); 61 | } 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /app/initializers/json-api-validator.ts: -------------------------------------------------------------------------------- 1 | import setupValidator from '@ember-data/json-api-validator/setup-ember-data-validations'; 2 | 3 | setupValidator(); 4 | 5 | export default { 6 | initialize() {} 7 | }; 8 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary') 10 | ]).then((urls) => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: 'ember-lts-2.12', 16 | npm: { 17 | devDependencies: { 18 | 'ember-source': '~2.12.0' 19 | } 20 | } 21 | }, 22 | { 23 | name: 'ember-lts-2.16', 24 | npm: { 25 | devDependencies: { 26 | 'ember-source': '~2.16.0' 27 | } 28 | } 29 | }, 30 | { 31 | name: 'ember-lts-2.18', 32 | npm: { 33 | devDependencies: { 34 | 'ember-source': '~2.18.0' 35 | } 36 | } 37 | }, 38 | { 39 | name: 'ember-release', 40 | npm: { 41 | devDependencies: { 42 | 'ember-source': urls[0] 43 | } 44 | } 45 | }, 46 | { 47 | name: 'ember-beta', 48 | npm: { 49 | devDependencies: { 50 | 'ember-source': urls[1] 51 | } 52 | } 53 | }, 54 | { 55 | name: 'ember-canary', 56 | npm: { 57 | devDependencies: { 58 | 'ember-source': urls[2] 59 | } 60 | } 61 | }, 62 | { 63 | name: 'ember-default', 64 | npm: { 65 | devDependencies: {} 66 | } 67 | } 68 | ] 69 | }; 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: '@ember-data/json-api-validator' 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ember-data/json-api-validator", 3 | "version": "0.0.0", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "MIT", 9 | "author": "", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "", 15 | "scripts": { 16 | "build": "ember build", 17 | "lint:js": "eslint .", 18 | "start": "ember serve", 19 | "test": "ember test", 20 | "test:all": "ember try:each", 21 | "prepublishOnly": "ember ts:precompile", 22 | "postpublish": "ember ts:clean" 23 | }, 24 | "peerDependencies": { 25 | "ember-data": "*", 26 | "ember-inflector": "*" 27 | }, 28 | "dependencies": { 29 | "ember-cli-babel": "^6.6.0" 30 | }, 31 | "devDependencies": { 32 | "@types/ember": "^3.0.2", 33 | "@types/ember-data": "^3.0.0", 34 | "@types/ember-qunit": "^3.4.0", 35 | "@types/ember-test-helpers": "^1.0.0", 36 | "@types/ember-testing-helpers": "^0.0.3", 37 | "@types/ember__test-helpers": "^0.7.1", 38 | "@types/qunit": "^2.5.3", 39 | "@types/rsvp": "^4.0.2", 40 | "broccoli-asset-rev": "^2.7.0", 41 | "ember-ajax": "^3.0.0", 42 | "ember-cli": "~3.3.0", 43 | "ember-cli-dependency-checker": "^2.0.0", 44 | "ember-cli-eslint": "^4.2.1", 45 | "ember-cli-htmlbars": "^2.0.1", 46 | "ember-cli-htmlbars-inline-precompile": "^1.0.0", 47 | "ember-cli-inject-live-reload": "^1.4.1", 48 | "ember-cli-qunit": "^4.3.2", 49 | "ember-cli-shims": "^1.2.0", 50 | "ember-cli-sri": "^2.1.0", 51 | "ember-cli-typescript": "^1.4.2", 52 | "ember-cli-uglify": "^2.0.0", 53 | "ember-data": "^3.4.0", 54 | "ember-disable-prototype-extensions": "^1.1.2", 55 | "ember-export-application-global": "^2.0.0", 56 | "ember-inflector": "^3.0.0", 57 | "ember-load-initializers": "^1.1.0", 58 | "ember-maybe-import-regenerator": "^0.1.6", 59 | "ember-qunit-assert-helpers": "^0.2.1", 60 | "ember-resolver": "^4.0.0", 61 | "ember-source": "~3.3.0", 62 | "ember-source-channel-url": "^1.0.1", 63 | "ember-try": "^0.2.23", 64 | "ember-welcome-page": "^3.0.0", 65 | "eslint-plugin-ember": "^5.0.0", 66 | "eslint-plugin-node": "^6.0.1", 67 | "json-typescript": "^1.0.1", 68 | "jsonapi-typescript": "^0.0.9", 69 | "loader.js": "^4.2.3", 70 | "qunit-dom": "^0.6.2", 71 | "typescript": "^3.0.3" 72 | }, 73 | "engines": { 74 | "node": "6.* || 8.* || >= 10.*" 75 | }, 76 | "ember-addon": { 77 | "configPath": "tests/dummy/config" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-gpu', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from './config/environment' 6 | * 7 | * For now these need to be managed by the developer 8 | * since different ember addons can materialize new entries. 9 | */ 10 | declare const config: { 11 | environment: any; 12 | modulePrefix: string; 13 | podModulePrefix: string; 14 | locationType: string; 15 | rootURL: string; 16 | }; 17 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/animal.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr } = DS; 4 | 5 | export default Model.extend({ 6 | kind: attr() 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/dog.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Model from './pet'; 3 | 4 | const { attr } = DS; 5 | 6 | export default Model.extend({ 7 | kind: attr(), 8 | age: attr() 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/models/flying-dog.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Model from './dog'; 3 | 4 | const { attr } = DS; 5 | 6 | export default Model.extend({ 7 | leap: attr() 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/models/person.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr, hasMany } = DS; 4 | 5 | export default Model.extend({ 6 | firstName: attr(), 7 | lastName: attr(), 8 | pets: hasMany('pet', { inverse: 'person', polymorphic: true }) 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/models/pet.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Model from './animal'; 3 | 4 | const { attr, belongsTo } = DS; 5 | 6 | export default Model.extend({ 7 | name: attr(), 8 | person: belongsTo('person', { inverse: 'pets', polymorphic: false }) 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{!-- The following component displays Ember's default welcome message. --}} 2 | {{welcome-page}} 3 | {{!-- Feel free to remove this! --}} 4 | 5 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/deep-copy.ts: -------------------------------------------------------------------------------- 1 | /* global WeakMap */ 2 | export default function deepCopy(obj: object) { 3 | return _deepCopy(obj, new WeakMap()); 4 | } 5 | 6 | function isPrimitive(value: any) { 7 | return typeof value !== 'object' || value === null; 8 | } 9 | 10 | function _deepCopy(oldObject: T, seen: WeakMap): T { 11 | if (Array.isArray(oldObject)) { 12 | return copyArray(oldObject, seen) as any; 13 | } else if (!isPrimitive(oldObject)) { 14 | return copyObject(oldObject, seen); 15 | } else { 16 | return oldObject; 17 | } 18 | } 19 | 20 | function copyObject(oldObject: T, seen: WeakMap): T { 21 | let newObject: any = {}; 22 | 23 | Object.keys(oldObject).forEach(key => { 24 | let value = oldObject[key]; 25 | let newValue = isPrimitive(value) ? value : seen.get(value); 26 | 27 | if (value && newValue === undefined) { 28 | newValue = newObject[key] = _deepCopy(value, seen); 29 | seen.set(value, newValue); 30 | } 31 | 32 | newObject[key] = newValue; 33 | }); 34 | 35 | return newObject; 36 | } 37 | 38 | function copyArray(oldArray: Array, seen: WeakMap): Array { 39 | let newArray = []; 40 | 41 | for (let i = 0; i < oldArray.length; i++) { 42 | let value = oldArray[i]; 43 | let newValue = isPrimitive(value) ? value : seen.get(value); 44 | 45 | if (value && newValue === undefined) { 46 | newValue = newArray[i] = _deepCopy(value, seen); 47 | seen.set(value, newValue); 48 | } 49 | 50 | newArray[i] = newValue; 51 | } 52 | 53 | return newArray; 54 | } 55 | -------------------------------------------------------------------------------- /tests/helpers/deep-merge.ts: -------------------------------------------------------------------------------- 1 | import isPlainObject from '@ember-data/json-api-validator/-private/utils/is-plain-object'; 2 | 3 | /* global WeakMap */ 4 | export default function deepMerge(base, ...objects) { 5 | return _deepMerge(base, objects); 6 | } 7 | 8 | function isPrimitive(value) { 9 | return typeof value !== 'object' || value === null; 10 | } 11 | 12 | function _deepMerge(base, objects) { 13 | for (let i = 0; i < objects.length; i++) { 14 | let target = objects[i]; 15 | let keys = Object.keys(target); 16 | 17 | for (let j = 0; j < keys.length; j++) { 18 | let key = keys[j]; 19 | let value = target[key]; 20 | let baseValue = base[key]; 21 | 22 | if (base[key] === undefined) { 23 | base[key] = value; 24 | } else if (Array.isArray(value) || Array.isArray(baseValue)) { 25 | if (Array.isArray(baseValue) && Array.isArray(value)) { 26 | base[key] = value; // we just clobber arrays 27 | continue; 28 | } 29 | throw new Error('Unmergeable values'); 30 | } else if (value === null || baseValue === null) { 31 | base[key] = value; 32 | } else if (!isPrimitive(baseValue) || !isPrimitive(value)) { 33 | if (isPlainObject(baseValue) && isPlainObject(value)) { 34 | _deepMerge(baseValue, [value]); 35 | } else { 36 | base[key] = value; 37 | } 38 | } else { 39 | base[key] = value; 40 | } 41 | } 42 | } 43 | 44 | return base; 45 | } 46 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.ts: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.ts: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import startApp from '../helpers/start-app'; 3 | import destroyApp from '../helpers/destroy-app'; 4 | import { resolve } from 'rsvp'; 5 | 6 | export default function(name, options = {}) { 7 | module(name, { 8 | beforeEach() { 9 | this.application = startApp(); 10 | 11 | if (options.beforeEach) { 12 | return options.beforeEach.apply(this, arguments); 13 | } 14 | }, 15 | 16 | afterEach() { 17 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 18 | return resolve(afterEach).then(() => destroyApp(this.application)); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/helpers/resolver.ts: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.ts: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { merge } from '@ember/polyfills'; 3 | import Application from '../../app'; 4 | import config from '../../config/environment'; 5 | 6 | export default function startApp(attrs) { 7 | let attributes = merge({}, config.APP); 8 | attributes = merge(attributes, attrs); // use defaults, but you can override; 9 | 10 | return run(() => { 11 | let application = Application.create(attributes); 12 | application.setupForTesting(); 13 | application.injectTestHelpers(); 14 | return application; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | import QUnit from 'qunit'; 6 | 7 | QUnit.assert.throwsWith = function throwsWith(testFn, message, label) { 8 | try { 9 | testFn(); 10 | this.pushResult({ 11 | result: false, 12 | actual: false, 13 | expected: true, 14 | message: `${label}\n\nExpected Error:\t${message}\n\nActual Error:\t<>` 15 | }); 16 | } catch (e) { 17 | /* eslint-disable no-console*/ 18 | console.log(e); 19 | if (e.message.indexOf(message) !== -1) { 20 | this.pushResult({ 21 | result: true, 22 | actual: true, 23 | expected: true, 24 | message: `${label}` 25 | }); 26 | } else { 27 | this.pushResult({ 28 | result: false, 29 | actual: false, 30 | expected: true, 31 | message: `${label}\n\nExpected Error:\t${message}\n\nActual Error:\t${e.message}` 32 | }); 33 | } 34 | } 35 | }; 36 | 37 | QUnit.assert.doesNotThrowWith = function doesNotThrowWith(testFn, message, label) { 38 | try { 39 | testFn(); 40 | this.pushResult({ 41 | result: true, 42 | actual: true, 43 | expected: true, 44 | message: `${label}` 45 | }); 46 | } catch (e) { 47 | if (e.message.indexOf(message) !== -1) { 48 | console.log(e); 49 | this.pushResult({ 50 | result: false, 51 | actual: false, 52 | expected: true, 53 | message: `${label}\n\nExpected No Error Containing:\t${message}\n\nDiscovered Error:\t${e.message}` 54 | }); 55 | } else { 56 | console.log('Unexpected Error', e); 57 | this.pushResult({ 58 | result: false, 59 | actual: true, 60 | expected: true, 61 | message: `${label}\n\n\tUnexpected Error:\t${e.message}` 62 | }); 63 | } 64 | } 65 | }; 66 | 67 | setApplication(Application.create(config.APP)); 68 | 69 | start(); 70 | -------------------------------------------------------------------------------- /tests/unit/invalid-document-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test, skip as todo } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import Store from 'ember-data/store'; 4 | import PersonModel from 'dummy/models/person'; 5 | import AnimalModel from 'dummy/models/animal'; 6 | import PetModel from 'dummy/models/pet'; 7 | import DogModel from 'dummy/models/dog'; 8 | import FlyingDogModel from 'dummy/models/flying-dog'; 9 | import setupEmberDataValidations from '@ember-data/json-api-validator/setup-ember-data-validations'; 10 | import { run } from '@ember/runloop'; 11 | import Ember from 'ember'; 12 | import deepCopy from '../helpers/deep-copy'; 13 | import deepMerge from '../helpers/deep-merge'; 14 | import { TestContext } from 'ember-test-helpers'; 15 | 16 | function buildDoc(base: any, extended: any) { 17 | return deepMerge({}, deepCopy(base), deepCopy(extended)); 18 | } 19 | 20 | let StoreClass = (Store as any).extend({}); 21 | 22 | setupEmberDataValidations(StoreClass); 23 | 24 | function registerModels(owner: any) { 25 | owner.register('model:person', PersonModel); 26 | owner.register('model:animal', AnimalModel); 27 | owner.register('model:pet', PetModel); 28 | owner.register('model:dog', DogModel); 29 | owner.register('model:flying-dog', FlyingDogModel); 30 | owner.register('service:store', StoreClass); 31 | } 32 | 33 | interface CustomTestContext { 34 | push: (data?: any) => typeof Ember.run; 35 | store: any; 36 | validator: any; 37 | disallowMetaOnlyDocuments: () => void; 38 | allowMetaOnlyDocuments: () => void; 39 | disallowMetaOnlyRelationships: () => void; 40 | allowMetaOnlyRelationships: () => void; 41 | enableStrictMode: () => void; 42 | disableStrictMode: () => void; 43 | } 44 | 45 | type ModuleContext = 46 | & NestedHooks 47 | & TestContext 48 | & CustomTestContext; 49 | 50 | module('Unit | Document', function(hooks: ModuleContext) { 51 | let push: (data?: any) => typeof Ember.run; 52 | let store: any; 53 | let validator: any; 54 | 55 | setupTest(hooks); 56 | 57 | hooks.beforeEach(function(this: ModuleContext) { 58 | Ember.Test.adapter.exception = e => { 59 | throw e; 60 | }; 61 | 62 | registerModels(this.owner); 63 | 64 | store = this.owner.lookup('service:store'); 65 | push = function push(data) { 66 | return run(() => { 67 | return store.push(data); 68 | }); 69 | }; 70 | validator = store.__validator; 71 | 72 | let disallowHook = () => true; 73 | let allowHook = () => false; 74 | 75 | this.disallowMetaOnlyDocuments = () => { 76 | validator.disallowMetaOnlyDocuments = disallowHook; 77 | }; 78 | this.allowMetaOnlyDocuments = () => { 79 | validator.disallowMetaOnlyDocuments = allowHook; 80 | }; 81 | this.disallowMetaOnlyRelationships = () => { 82 | validator.disallowMetaOnlyRelationships = disallowHook; 83 | }; 84 | this.allowMetaOnlyRelationships = () => { 85 | validator.disallowMetaOnlyRelationships = allowHook; 86 | }; 87 | this.enableStrictMode = () => { 88 | validator.strictMode = true; 89 | }; 90 | this.disableStrictMode = () => { 91 | validator.strictMode = false; 92 | }; 93 | }); 94 | 95 | module('Members', function() { 96 | test('a document MUST be an object', function(this: ModuleContext, assert) { 97 | this.allowMetaOnlyDocuments(); 98 | const VALID_DOC_ASSERT = ' is not a valid json-api document.'; 99 | 100 | assert.throwsWith( 101 | () => { 102 | push(); 103 | }, 104 | VALID_DOC_ASSERT, 105 | 'we throw for undefined' 106 | ); 107 | assert.throwsWith( 108 | () => { 109 | push(new Date()); 110 | }, 111 | VALID_DOC_ASSERT, 112 | 'we throw for a Date' 113 | ); 114 | assert.throwsWith( 115 | () => { 116 | push(null); 117 | }, 118 | VALID_DOC_ASSERT, 119 | 'we throw for null' 120 | ); 121 | assert.throwsWith( 122 | () => { 123 | push(true); 124 | }, 125 | VALID_DOC_ASSERT, 126 | 'we throw for Booleans' 127 | ); 128 | assert.throwsWith( 129 | () => { 130 | push('true'); 131 | }, 132 | VALID_DOC_ASSERT, 133 | 'we throw for Strings' 134 | ); 135 | assert.throwsWith( 136 | () => { 137 | push(1); 138 | }, 139 | VALID_DOC_ASSERT, 140 | 'we throw for numbers' 141 | ); 142 | assert.doesNotThrowWith( 143 | () => { 144 | push({ data: null, meta: { pages: 0 } }); 145 | }, 146 | VALID_DOC_ASSERT, 147 | 'we do not throw for {}' 148 | ); 149 | }); 150 | 151 | test('a document MUST contain one of `data`, `errors`, or `meta` as a member', function(this: ModuleContext, assert) { 152 | this.allowMetaOnlyDocuments(); 153 | const VALID_MEMBERS_ASSERT = 154 | 'A json-api document MUST contain one of `data`, `meta` or `errors` as a member.'; 155 | 156 | assert.throwsWith( 157 | () => { 158 | push({}); 159 | }, 160 | VALID_MEMBERS_ASSERT, 161 | 'we throw for empty' 162 | ); 163 | assert.throwsWith( 164 | () => { 165 | push({ foo: 'bar' }); 166 | }, 167 | VALID_MEMBERS_ASSERT, 168 | 'we throw when we have none of the members' 169 | ); 170 | }); 171 | 172 | test('a document MUST contain one of `data`, `errors`, or `meta` as a non-null member', function(this: ModuleContext, assert) { 173 | this.allowMetaOnlyDocuments(); 174 | const VALID_MEMBERS_ASSERT = 175 | 'A json-api document MUST contain one of `data`, `meta` or `errors` as a non-null member.'; 176 | 177 | assert.throwsWith( 178 | () => { 179 | push({ data: null, meta: undefined }); 180 | }, 181 | VALID_MEMBERS_ASSERT, 182 | 'we throw when the members are null or undefined' 183 | ); 184 | assert.throwsWith( 185 | () => { 186 | push({ data: null, meta: null }); 187 | }, 188 | VALID_MEMBERS_ASSERT, 189 | 'we throw when all members are null or undefined' 190 | ); 191 | assert.doesNotThrowWith( 192 | () => { 193 | push({ data: null, meta: { pages: 0 } }); 194 | }, 195 | VALID_MEMBERS_ASSERT, 196 | 'we do not throw when at least one member is available' 197 | ); 198 | }); 199 | 200 | test('a document MUST NOT contain both `data` and `errors` as members', function(assert) { 201 | const VALID_DATA_ASSERT = 202 | 'A json-api document MUST NOT contain both `data` and `errors` as a members.'; 203 | 204 | assert.throwsWith( 205 | () => { 206 | push({ 207 | data: { type: 'animal', id: '1', attributes: {} }, 208 | errors: null, 209 | }); 210 | }, 211 | VALID_DATA_ASSERT, 212 | 'we throw when the members are null or undefined' 213 | ); 214 | }); 215 | 216 | test('a document MAY contain `jsonapi` `links` and `included` as members ', function(assert) { 217 | const VALID_MEMBER_ASSERT = ''; 218 | let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 219 | let linksDoc = buildDoc(fakeDoc, { links: { self: 'http://api.example.com/animal/1' } }); 220 | let jsonApiDoc = buildDoc(fakeDoc, { jsonapi: { version: '1.0' } }); 221 | let includedDoc = buildDoc(fakeDoc, { included: [] }); 222 | let allDoc = buildDoc(fakeDoc, { 223 | links: { self: 'http://api.example.com/animal/1' }, 224 | jsonapi: { version: '1.0' }, 225 | included: [], 226 | }); 227 | 228 | assert.doesNotThrowWith( 229 | () => { 230 | push(linksDoc); 231 | }, 232 | VALID_MEMBER_ASSERT, 233 | 'we do not throw for links' 234 | ); 235 | assert.doesNotThrowWith( 236 | () => { 237 | push(jsonApiDoc); 238 | }, 239 | VALID_MEMBER_ASSERT, 240 | 'we do not throw for jsonapi' 241 | ); 242 | assert.doesNotThrowWith( 243 | () => { 244 | push(includedDoc); 245 | }, 246 | VALID_MEMBER_ASSERT, 247 | 'we do not throw for included' 248 | ); 249 | assert.doesNotThrowWith( 250 | () => { 251 | push(allDoc); 252 | }, 253 | VALID_MEMBER_ASSERT, 254 | 'we do not throw for all present' 255 | ); 256 | }); 257 | 258 | test('a document MUST NOT have the `included` member if `data` is not also present', function(this: ModuleContext, assert) { 259 | this.disallowMetaOnlyDocuments(); 260 | this.enableStrictMode(); 261 | const INVALID_INCLUDED_ASSERT = 262 | 'A json-api document MUST NOT contain `included` as a member unless `data` is also present.'; 263 | let dataAndIncludedDoc = { 264 | data: { type: 'animal', id: '1', attributes: {} }, 265 | included: [], 266 | }; 267 | let includedOnlyDoc = { 268 | meta: { pages: 0 }, 269 | included: [], 270 | }; 271 | 272 | assert.doesNotThrowWith( 273 | () => { 274 | push(dataAndIncludedDoc); 275 | }, 276 | INVALID_INCLUDED_ASSERT, 277 | 'we do not throw for included' 278 | ); 279 | assert.throwsWith( 280 | () => { 281 | push(includedOnlyDoc); 282 | }, 283 | INVALID_INCLUDED_ASSERT, 284 | 'we do not throw for included' 285 | ); 286 | }); 287 | 288 | test('a document MUST NOT contain any non-spec members', function(this: ModuleContext, assert) { 289 | this.enableStrictMode(); 290 | const VALID_MEMBER_ASSERT = 291 | 'is not a valid member of a json-api document.'; 292 | let baseDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 293 | let fakeDoc1 = buildDoc(baseDoc, { unknownMember: undefined }); 294 | let fakeDoc2 = buildDoc(baseDoc, { unknownMember: null }); 295 | let fakeDoc3 = buildDoc(baseDoc, { unknownMember: {} }); 296 | 297 | assert.throwsWith( 298 | () => { 299 | push(fakeDoc1); 300 | }, 301 | VALID_MEMBER_ASSERT, 302 | 'We throw for unexpected members' 303 | ); 304 | assert.throwsWith( 305 | () => { 306 | push(fakeDoc2); 307 | }, 308 | VALID_MEMBER_ASSERT, 309 | 'We throw for unexpected members' 310 | ); 311 | assert.throwsWith( 312 | () => { 313 | push(fakeDoc3); 314 | }, 315 | VALID_MEMBER_ASSERT, 316 | 'We throw for unexpected members' 317 | ); 318 | }); 319 | 320 | test('(loose-mode) a document SHOULD NOT contain any non-spec members', function(this: ModuleContext, assert) { 321 | this.disableStrictMode(); 322 | const VALID_MEMBER_ASSERT = 323 | 'is not a valid member of a json-api document.'; 324 | let baseDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 325 | let fakeDoc1 = buildDoc(baseDoc, { unknownMember: undefined }); 326 | let fakeDoc2 = buildDoc(baseDoc, { unknownMember: null }); 327 | let fakeDoc3 = buildDoc(baseDoc, { unknownMember: {} }); 328 | 329 | assert.doesNotThrowWith( 330 | () => { 331 | push(fakeDoc1); 332 | }, 333 | VALID_MEMBER_ASSERT, 334 | 'We do not throw for unexpected members' 335 | ); 336 | assert.expectWarning( 337 | () => { 338 | push(fakeDoc1); 339 | }, 340 | VALID_MEMBER_ASSERT, 341 | 'We warn for unexpected members' 342 | ); 343 | assert.doesNotThrowWith( 344 | () => { 345 | push(fakeDoc2); 346 | }, 347 | VALID_MEMBER_ASSERT, 348 | 'We do not throw for unexpected members' 349 | ); 350 | assert.expectWarning( 351 | () => { 352 | push(fakeDoc2); 353 | }, 354 | VALID_MEMBER_ASSERT, 355 | 'We warn for unexpected members' 356 | ); 357 | assert.doesNotThrowWith( 358 | () => { 359 | push(fakeDoc3); 360 | }, 361 | VALID_MEMBER_ASSERT, 362 | 'We do not throw for unexpected members' 363 | ); 364 | assert.expectWarning( 365 | () => { 366 | push(fakeDoc3); 367 | }, 368 | VALID_MEMBER_ASSERT, 369 | 'We warn for unexpected members' 370 | ); 371 | }); 372 | }); 373 | 374 | module('Top-level jsonapi member', function() { 375 | test('MUST contain version', function(assert) { 376 | let baseDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 377 | let fakeDoc1 = buildDoc(baseDoc, { jsonapi: undefined }); 378 | let fakeDoc2 = buildDoc(baseDoc, { jsonapi: null }); 379 | let fakeDoc3 = buildDoc(baseDoc, { jsonapi: {} }); 380 | let fakeDoc4 = buildDoc(baseDoc, { jsonapi: { version: undefined } }); 381 | let fakeDoc5 = buildDoc(baseDoc, { jsonapi: { version: null } }); 382 | let fakeDoc6 = buildDoc(baseDoc, { jsonapi: { version: '1.0.0' } }); 383 | 384 | assert.throwsWith( 385 | () => { 386 | push(fakeDoc1); 387 | }, 388 | `'.jsonapi' MUST be an object if present, found value of type undefined`, 389 | 'We throw when the value is explicitly undefined' 390 | ); 391 | assert.throwsWith( 392 | () => { 393 | push(fakeDoc2); 394 | }, 395 | `'.jsonapi' MUST be an object if present, found value of type Null`, 396 | 'We throw when the value is null' 397 | ); 398 | assert.throwsWith( 399 | () => { 400 | push(fakeDoc3); 401 | }, 402 | `expected a 'version' member to be present in the 'document.jsonapi' object`, 403 | 'We throw when missing version' 404 | ); 405 | assert.throwsWith( 406 | () => { 407 | push(fakeDoc4); 408 | }, 409 | `expected the 'version' member present in the 'document.jsonapi' object to be a string, found value of type undefined`, 410 | 'We throw when missing version' 411 | ); 412 | assert.throwsWith( 413 | () => { 414 | push(fakeDoc5); 415 | }, 416 | `expected the 'version' member present in the 'document.jsonapi' object to be a string, found value of type Null`, 417 | 'We throw when missing version' 418 | ); 419 | assert.doesNotThrowWith( 420 | () => { 421 | push(fakeDoc6); 422 | }, 423 | `expected a 'version' member to be present in the 'document.jsonapi'`, 424 | 'We do not throw when version is present' 425 | ); 426 | }); 427 | 428 | test('MAY contain meta', function(assert) { 429 | let baseDoc = { 430 | data: { type: 'animal', id: '1', attributes: {} }, 431 | jsonapi: { version: '1.0.0' }, 432 | }; 433 | let fakeDoc1 = buildDoc(baseDoc, { jsonapi: { meta: undefined } }); 434 | let fakeDoc2 = buildDoc(baseDoc, { jsonapi: { meta: null } }); 435 | let fakeDoc3 = buildDoc(baseDoc, { jsonapi: { meta: {} } }); 436 | let fakeDoc4 = buildDoc(baseDoc, { 437 | jsonapi: { meta: { features: ['rfc-293'] } }, 438 | }); 439 | 440 | assert.throwsWith( 441 | () => { 442 | push(fakeDoc1); 443 | }, 444 | `'.jsonapi.meta' MUST be an object when present: found value of type undefined`, 445 | 'We throw when the value is explicitly undefined' 446 | ); 447 | assert.throwsWith( 448 | () => { 449 | push(fakeDoc2); 450 | }, 451 | `'.jsonapi.meta' MUST be an object when present: found value of type Null`, 452 | 'We throw when the value is null' 453 | ); 454 | assert.throwsWith( 455 | () => { 456 | push(fakeDoc3); 457 | }, 458 | `'.jsonapi.meta' MUST have at least one member: found an empty object.`, 459 | 'We throw when we have no keys' 460 | ); 461 | assert.doesNotThrowWith( 462 | () => { 463 | push(fakeDoc4); 464 | }, 465 | `'.jsonapi.meta' MUST`, 466 | 'We do not throw when at least one key is present' 467 | ); 468 | }); 469 | todo('MUST NOT contain other members', function(assert) { 470 | assert.notOk('Not Implemented'); 471 | }); 472 | }); 473 | 474 | module('Top-level meta', function() { 475 | test('meta must be well-formed', function(assert) { 476 | let baseDoc = { 477 | data: { type: 'animal', id: '1', attributes: {} }, 478 | }; 479 | let fakeDoc1 = buildDoc(baseDoc, { meta: undefined }); 480 | let fakeDoc2 = buildDoc(baseDoc, { meta: null }); 481 | let fakeDoc3 = buildDoc(baseDoc, { meta: {} }); 482 | let fakeDoc4 = buildDoc(baseDoc, { 483 | meta: { features: ['rfc-293'] }, 484 | }); 485 | 486 | assert.throwsWith( 487 | () => { 488 | push(fakeDoc1); 489 | }, 490 | `'.meta' MUST be an object when present: found value of type undefined`, 491 | 'We throw when the value is explicitly undefined' 492 | ); 493 | assert.throwsWith( 494 | () => { 495 | push(fakeDoc2); 496 | }, 497 | `'.meta' MUST be an object when present: found value of type Null`, 498 | 'We throw when the value is null' 499 | ); 500 | assert.throwsWith( 501 | () => { 502 | push(fakeDoc3); 503 | }, 504 | `'.meta' MUST have at least one member: found an empty object.`, 505 | 'We throw when we have no keys' 506 | ); 507 | assert.doesNotThrowWith( 508 | () => { 509 | push(fakeDoc4); 510 | }, 511 | `'.meta' MUST`, 512 | 'We do not throw when at least one key is present' 513 | ); 514 | }); 515 | 516 | test('(ember-data-quirk) a json-api document MUST have `data` or `errors` in addition to `meta`', function(this: ModuleContext, assert) { 517 | this.disallowMetaOnlyDocuments(); 518 | const META_ONLY_ASSERT = 519 | "'.meta' MUST NOT be the only member of '. Expected `data` or `errors` as a sibling."; 520 | 521 | assert.throwsWith( 522 | () => { 523 | push({ data: undefined, meta: { pages: 0 } }); 524 | }, 525 | META_ONLY_ASSERT, 526 | 'we throw when other available members are undefined' 527 | ); 528 | assert.throwsWith( 529 | () => { 530 | push({ data: null, meta: { pages: 0 } }); 531 | }, 532 | META_ONLY_ASSERT, 533 | 'we throw when other available members are null' 534 | ); 535 | assert.throwsWith( 536 | () => { 537 | push({ meta: { pages: 0 } }); 538 | }, 539 | META_ONLY_ASSERT, 540 | 'we throw for meta-only documents' 541 | ); 542 | assert.doesNotThrowWith( 543 | () => { 544 | push({ 545 | data: { type: 'animal', id: '1', attributes: {} }, 546 | meta: { pages: 0 }, 547 | }); 548 | }, 549 | META_ONLY_ASSERT, 550 | 'we do not throw when other members are defined' 551 | ); 552 | }); 553 | }); 554 | 555 | module('Top-level Links', function() { 556 | test('links MUST be an object if present', function(assert) { 557 | let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 558 | let linksDoc1 = buildDoc(fakeDoc, { links: [] }); 559 | let linksDoc2 = buildDoc(fakeDoc, { links: null }); 560 | let linksDoc3 = buildDoc(fakeDoc, { links: undefined }); 561 | let linksDoc4 = buildDoc(fakeDoc, { links: {} }); 562 | let linksDoc5 = buildDoc(fakeDoc, { 563 | links: { 564 | self: 'https://api.example.com/animal/1', 565 | }, 566 | }); 567 | 568 | assert.throwsWith( 569 | () => { 570 | push(linksDoc1); 571 | }, 572 | `'.links' MUST be an object when present: found value of type Array`, 573 | 'we throw for links as array' 574 | ); 575 | 576 | assert.throwsWith( 577 | () => { 578 | push(linksDoc2); 579 | }, 580 | `'.links' MUST be an object when present: found value of type Null`, 581 | 'we throw for links as null' 582 | ); 583 | 584 | assert.throwsWith( 585 | () => { 586 | push(linksDoc3); 587 | }, 588 | `'.links' MUST be an object when present: found value of type undefined`, 589 | 'we throw for links as undefined' 590 | ); 591 | 592 | assert.throwsWith( 593 | () => { 594 | push(linksDoc4); 595 | }, 596 | `'.links' MUST have at least one member: found an empty object.`, 597 | 'we throw for links as an empty object' 598 | ); 599 | 600 | assert.doesNotThrowWith( 601 | () => { 602 | push(linksDoc5); 603 | }, 604 | `'.links' MUST have at least one member: found an empty object.`, 605 | 'we do not throw for links that have a valid member' 606 | ); 607 | }); 608 | 609 | todo( 610 | 'links MAY contain `self`, `related` and the pagination links `first`, `last`, `prev` and `next`', 611 | function(assert) { 612 | assert.notOk('Not Implemented'); 613 | } 614 | ); 615 | 616 | todo( 617 | 'included `self` and `related` links MUST either be string URLs or an object with members `href` (a string URL) and an optional `meta` object', 618 | function(assert) { 619 | assert.notOk('Not Implemented'); 620 | } 621 | ); 622 | 623 | todo( 624 | 'included pagination links MUST either be null, string URLs or an object with members `href` (a string URL) and an optional `meta` object', 625 | function(assert) { 626 | assert.notOk('Not Implemented'); 627 | } 628 | ); 629 | 630 | todo('(strict-mode) links MAY NOT contain any non-spec members', function( 631 | assert 632 | ) { 633 | assert.notOk('Not Implemented'); 634 | }); 635 | 636 | todo('a document MUST ', function(assert) { 637 | assert.notOk('Not Implemented'); 638 | }); 639 | }); 640 | 641 | module('Data', function() { 642 | todo( 643 | 'Collections MUST be uniformly resource-objects or resource-identifiers', 644 | function(assert) { 645 | assert.notOk('Not Implemented'); 646 | } 647 | ); 648 | 649 | todo('(strict mode) Collection MUST be of a uniform type', function( 650 | assert 651 | ) { 652 | assert.notOk('Not Implemented'); 653 | }); 654 | }); 655 | 656 | module('Included', function() { 657 | todo( 658 | '(strict mode) entries in included MUST NOT be resource-identifiers', 659 | function(assert) { 660 | assert.notOk('Not Implemented'); 661 | } 662 | ); 663 | 664 | todo('(strict mode) entries MUST trace linkage to primary data', function( 665 | assert 666 | ) { 667 | assert.notOk('Not Implemented'); 668 | }); 669 | 670 | todo('a document MUST ', function(assert) { 671 | assert.notOk('Not Implemented'); 672 | }); 673 | }); 674 | 675 | module('Errors', function() { 676 | todo('a document MUST ', function(assert) { 677 | assert.notOk('Not Implemented'); 678 | }); 679 | }); 680 | }); 681 | -------------------------------------------------------------------------------- /tests/unit/invalid-resource-test.ts: -------------------------------------------------------------------------------- 1 | import { module, skip as todo } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import Store from 'ember-data/store'; 4 | import PersonModel from 'dummy/models/person'; 5 | import AnimalModel from 'dummy/models/animal'; 6 | import PetModel from 'dummy/models/pet'; 7 | import DogModel from 'dummy/models/dog'; 8 | import FlyingDogModel from 'dummy/models/flying-dog'; 9 | import setupEmberDataValidations from '@ember-data/json-api-validator/setup-ember-data-validations'; 10 | import { run } from '@ember/runloop'; 11 | import deepCopy from '../helpers/deep-copy'; 12 | import deepMerge from '../helpers/deep-merge'; 13 | import { TestContext } from 'ember-test-helpers'; 14 | import Ember from 'ember'; 15 | 16 | function buildDoc(base: any, extended: any) { 17 | return deepMerge({}, deepCopy(base), deepCopy(extended)); 18 | } 19 | 20 | let StoreClass = Store.extend({}); 21 | 22 | setupEmberDataValidations(StoreClass); 23 | 24 | function registerModels(owner: any) { 25 | owner.register('model:person', PersonModel); 26 | owner.register('model:animal', AnimalModel); 27 | owner.register('model:pet', PetModel); 28 | owner.register('model:dog', DogModel); 29 | owner.register('model:flying-dog', FlyingDogModel); 30 | owner.register('service:store', StoreClass); 31 | } 32 | 33 | interface CustomTestContext { 34 | push: (data: any) => typeof Ember.run; 35 | store: any; 36 | } 37 | 38 | type ModuleContext = 39 | & NestedHooks 40 | & TestContext 41 | & CustomTestContext; 42 | 43 | 44 | module('Unit | Resource', function(hooks: ModuleContext) { 45 | let push: (data: any) => typeof Ember.run; 46 | let store: any; 47 | 48 | setupTest(hooks); 49 | 50 | hooks.beforeEach(function(this: ModuleContext, assert) { 51 | store = this.owner.lookup('service:store'); 52 | push = function push(data) { 53 | return run(() => { 54 | return store.push(data); 55 | }); 56 | }; 57 | registerModels(this.owner); 58 | 59 | assert.throwsWith = function throwsWith(testFn, message, label) { 60 | try { 61 | testFn(); 62 | assert.ok( 63 | false, 64 | `${label}\n\nExpected Error:\t${message}\n\nActual Error:\t<>` 65 | ); 66 | } catch (e) { 67 | if (e.message.indexOf(message) !== -1) { 68 | assert.ok(true, `${label}`); 69 | } else { 70 | assert.ok( 71 | false, 72 | `${label}\n\nExpected Error:\t${message}\n\nActual Error:\t${ 73 | e.message 74 | }` 75 | ); 76 | } 77 | } 78 | }; 79 | }); 80 | 81 | module('Members', function() { 82 | todo('a resource MUST be an object', function(assert) { 83 | assert.notOk('Not Implemented'); 84 | }); 85 | }); 86 | 87 | module('Type', function() { 88 | todo('a resource MUST ', function(assert) { 89 | assert.notOk('Not Implemented'); 90 | }); 91 | }); 92 | 93 | module('Id', function() {}); 94 | 95 | module('Links', function() { 96 | todo('links MUST be an object if present', function(assert) { 97 | const VALID_MEMBER_ASSERT = 'some actual error'; 98 | 99 | let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } }; 100 | let linksDoc1 = buildDoc(fakeDoc, { data: { links: [] } }); 101 | let linksDoc2 = buildDoc(fakeDoc, { data: { links: null } }); 102 | let linksDoc3 = buildDoc(fakeDoc, { data: { links: undefined } }); 103 | let linksDoc4 = buildDoc(fakeDoc, { data: { links: {} } }); 104 | let linksDoc5 = buildDoc(fakeDoc, { 105 | data: { 106 | links: { 107 | self: 'https://api.example.com/animal/1', 108 | }, 109 | }, 110 | }); 111 | 112 | assert.throwsWith( 113 | () => { 114 | push(linksDoc1); 115 | }, 116 | VALID_MEMBER_ASSERT, 117 | 'we throw for links as array' 118 | ); 119 | 120 | assert.doesNotThrowWith( 121 | () => { 122 | push(linksDoc2); 123 | }, 124 | VALID_MEMBER_ASSERT, 125 | 'we throw for links as null' 126 | ); 127 | 128 | assert.throwsWith( 129 | () => { 130 | push(linksDoc3); 131 | }, 132 | VALID_MEMBER_ASSERT, 133 | 'we throw for links as undefined' 134 | ); 135 | 136 | assert.throwsWith( 137 | () => { 138 | push(linksDoc4); 139 | }, 140 | VALID_MEMBER_ASSERT, 141 | 'we throw for links as an empty object' 142 | ); 143 | 144 | assert.doesNotThrowWith( 145 | () => { 146 | push(linksDoc5); 147 | }, 148 | VALID_MEMBER_ASSERT, 149 | 'we do not throw for links that have a valid member' 150 | ); 151 | }); 152 | 153 | todo( 154 | 'links MAY contain `self`, `related` and the pagination links `first`, `last`, `prev` and `next`', 155 | function(assert) { 156 | assert.notOk('Not Implemented'); 157 | } 158 | ); 159 | 160 | todo( 161 | 'included `self` and `related` links MUST either be string URLs or an object with members `href` (a string URL) and an optional `meta` object', 162 | function(assert) { 163 | assert.notOk('Not Implemented'); 164 | } 165 | ); 166 | 167 | todo( 168 | 'included pagination links MUST either be null, string URLs or an object with members `href` (a string URL) and an optional `meta` object', 169 | function(assert) { 170 | assert.notOk('Not Implemented'); 171 | } 172 | ); 173 | 174 | todo('(strict-mode) links MAY NOT contain any non-spec members', function( 175 | assert 176 | ) { 177 | assert.notOk('Not Implemented'); 178 | }); 179 | 180 | todo('a document MUST ', function(assert) { 181 | assert.notOk('Not Implemented'); 182 | }); 183 | }); 184 | 185 | module('Attributes', function() { 186 | todo('a resource MUST ', function(assert) { 187 | assert.notOk('Not Implemented'); 188 | }); 189 | }); 190 | 191 | module('Relationships', function() { 192 | todo('a resource MUST ', function(assert) { 193 | assert.notOk('Not Implemented'); 194 | }); 195 | }); 196 | 197 | module('Errors', function() { 198 | todo('a resource MUST ', function(assert) { 199 | assert.notOk('Not Implemented'); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowJs": true, 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "strictNullChecks": true, 11 | "strictPropertyInitialization": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noEmitOnError": false, 17 | "noEmit": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "baseUrl": ".", 21 | "module": "es6", 22 | "paths": { 23 | "dummy/tests/*": [ 24 | "tests/*" 25 | ], 26 | "dummy/*": [ 27 | "tests/dummy/app/*", 28 | "app/*" 29 | ], 30 | "@ember-data/json-api-validator": [ 31 | "addon" 32 | ], 33 | "@ember-data/json-api-validator/*": [ 34 | "addon/*" 35 | ], 36 | "@ember-data/json-api-validator/test-support": [ 37 | "addon-test-support" 38 | ], 39 | "@ember-data/json-api-validator/test-support/*": [ 40 | "addon-test-support/*" 41 | ], 42 | "*": [ 43 | "types/*" 44 | ] 45 | } 46 | }, 47 | "include": [ 48 | "app/**/*", 49 | "addon/**/*", 50 | "tests/**/*", 51 | "types/**/*", 52 | "test-support/**/*", 53 | "addon-test-support/**/*" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /types/dummy/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /types/ember-data.d.ts: -------------------------------------------------------------------------------- 1 | import JSONAPIValidator from "@ember-data/json-api-validator/-private/validator"; 2 | import { Document } from "jsonapi-typescript"; 3 | import { DocumentError } from "@ember-data/json-api-validator/-private/errors/document-error"; 4 | 5 | /** 6 | * Catch-all for ember-data. 7 | */ 8 | declare module 'ember-data' { 9 | interface ModelRegistry { 10 | [key: string]: any; 11 | } 12 | 13 | interface ISchemaEntry { 14 | key: string; 15 | schema: string; 16 | } 17 | 18 | interface ISchemaAttributes { 19 | attr?: string[]; 20 | } 21 | 22 | type ISchema = 23 | & { [kind: string]: Array; } 24 | & ISchemaAttributes; 25 | 26 | interface IErrorOptions { 27 | key: string; 28 | value: string; 29 | path: string; 30 | code: number; 31 | member?: string; 32 | document?: Document; 33 | validator?: JSONAPIValidator; 34 | } 35 | 36 | interface IIssues { 37 | errors: DocumentError[]; 38 | warnings: string[]; 39 | } 40 | 41 | interface IValidationContext { 42 | validator: JSONAPIValidator; 43 | document: Document | null | Date; 44 | target: Document; 45 | issues: IIssues; 46 | path: string; 47 | requiredSiblings?: any 48 | } 49 | 50 | interface IObject { 51 | [member: string]: any 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /types/qunit-assert-helpers.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace QUnitAssertHelpers { 2 | type TestFnThatMayThrow = () => void; 3 | 4 | interface Matchers { 5 | throwsWith( 6 | fn: TestFnThatMayThrow, 7 | messageContains: string, 8 | assertDescription?: string 9 | ): void; 10 | 11 | doesNotThrowWith( 12 | fn: TestFnThatMayThrow, 13 | messageContains: string, 14 | assertDescription?: string 15 | ): void; 16 | 17 | expectWarning( 18 | fn: TestFnThatMayThrow, 19 | messageContains: string, 20 | assertDescription?: string 21 | ): void 22 | } 23 | } 24 | 25 | interface Assert extends QUnitAssertHelpers.Matchers { 26 | 27 | } -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-data/json-api-validator/6df14769452a6d10fc2557c7104c089137ee1817/vendor/.gitkeep --------------------------------------------------------------------------------