diff --git a/packages/@orbit/record-cache/test/operation-processors/schema-validation-processor-test.ts b/packages/@orbit/record-cache/test/operation-processors/schema-validation-processor-test.ts index fbe04dcc..774afc3b 100644 --- a/packages/@orbit/record-cache/test/operation-processors/schema-validation-processor-test.ts +++ b/packages/@orbit/record-cache/test/operation-processors/schema-validation-processor-test.ts @@ -4,8 +4,10 @@ import { RecordSchema, RecordSchemaSettings, RelationshipNotDefined, + StandardRecordValidators, ValidationError } from '@orbit/records'; +import { ValidationIssue } from '@orbit/validators'; import { ExampleSyncRecordCache } from '../support/example-sync-record-cache'; import { SyncSchemaValidationProcessor } from '../../src/operation-processors/sync-schema-validation-processor'; @@ -70,180 +72,292 @@ module('SchemaValidationProcessor', function (hooks) { test('addRecord with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.addRecord(unknown)); + cache.update((t) => t.addRecord(unknown)); }, unknownError); }); test('updateRecord with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.updateRecord(unknown)); + cache.update((t) => t.updateRecord(unknown)); }, unknownError); }); test('removeRecord with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.removeRecord(unknown)); + cache.update((t) => t.removeRecord(unknown)); }, unknownError); }); test('replaceKey with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceKey(unknown, 'key', 'value')); + cache.update((t) => t.replaceKey(unknown, 'key', 'value')); }, unknownError); }); test('replaceAttribute with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceAttribute(unknown, 'attribute', 'value')); + cache.update((t) => t.replaceAttribute(unknown, 'attribute', 'value')); }, unknownError); }); test('addToRelatedRecords with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.addToRelatedRecords(unknown, 'children', node)); + cache.update((t) => t.addToRelatedRecords(unknown, 'children', node)); }, unknownError); }); test('addToRelatedRecords with an unknown related model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.addToRelatedRecords(node, 'children', unknown)); + cache.update((t) => t.addToRelatedRecords(node, 'children', unknown)); }, unknownError); }); test('addToRelatedRecords with a relationship not defined in the schema', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.addToRelatedRecords({ type: 'node', id: '1' }, 'sibling', { - type: 'node', - id: '2' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.addToRelatedRecords({ type: 'node', id: '1' }, 'sibling', { + type: 'node', + id: '2' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship 'sibling' for type 'node' is not defined in schema` + } + ] + ) + ); }); test('addToRelatedRecord with a related record with an invalid type for a non-polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.addToRelatedRecords({ type: 'node', id: '1' }, 'children', { - type: 'person', - id: '1' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.addToRelatedRecords({ type: 'node', id: '1' }, 'children', { + type: 'person', + id: '1' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); test('addToRelatedRecords with a related record with an invalid type for a polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.addToRelatedRecords({ type: 'person', id: '1' }, 'pets', { - type: 'person', - id: '2' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.addToRelatedRecords({ type: 'person', id: '1' }, 'pets', { + type: 'person', + id: '2' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); test('removeFromRelatedRecords with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.removeFromRelatedRecords(unknown, 'children', node)); + cache.update((t) => + t.removeFromRelatedRecords(unknown, 'children', node) + ); }, unknownError); }); test('removeFromRelatedRecords with an unknown related model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.removeFromRelatedRecords(node, 'children', unknown)); + cache.update((t) => + t.removeFromRelatedRecords(node, 'children', unknown) + ); }, unknownError); }); test('replaceRelatedRecords with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceRelatedRecords(unknown, 'children', [node])); + cache.update((t) => t.replaceRelatedRecords(unknown, 'children', [node])); }, unknownError); }); test('replaceRelatedRecords with an unknown related model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceRelatedRecords(node, 'children', [unknown])); + cache.update((t) => t.replaceRelatedRecords(node, 'children', [unknown])); }, unknownError); }); test('replaceRelatedRecords with a relationship not defined in the schema', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecords({ type: 'node', id: '1' }, 'siblings', [ - { type: 'node', id: '2' } - ]) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecords({ type: 'node', id: '1' }, 'siblings', [ + { type: 'node', id: '2' } + ]) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship 'siblings' for type 'node' is not defined in schema` + } + ] + ) + ); }); test('replaceRelatedRecords with a related record with an invalid type for a non-polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecords({ type: 'node', id: '1' }, 'children', [ - { type: 'person', id: '1' } - ]) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecords({ type: 'node', id: '1' }, 'children', [ + { type: 'person', id: '1' } + ]) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship data is invalid\n - relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); test('replaceRelatedRecords with a related record with an invalid type for a polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecords({ type: 'person', id: '1' }, 'pets', [ - { type: 'person', id: '2' } - ]) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecords({ type: 'person', id: '1' }, 'pets', [ + { type: 'person', id: '2' } + ]) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship data is invalid\n - relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); test('replaceRelatedRecord with an unknown model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceRelatedRecord(unknown, 'parent', node)); + cache.update((t) => t.replaceRelatedRecord(unknown, 'parent', node)); }, unknownError); }); test('replaceRelatedRecord with an unknown related model type', (assert) => { assert.throws(() => { - cache.patch((t) => t.replaceRelatedRecord(node, 'parent', unknown)); + cache.update((t) => t.replaceRelatedRecord(node, 'parent', unknown)); }, unknownError); }); test('replaceRelatedRecord with a null related model', (assert) => { - cache.patch((t) => t.replaceRelatedRecord(node, 'parent', null)); + cache.update((t) => t.replaceRelatedRecord(node, 'parent', null)); assert.ok(true, 'no error is thrown'); }); test('replaceRelatedRecord with a relationship not defined in the schema', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecord({ type: 'node', id: '1' }, 'mother', { - type: 'node', - id: '1' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecord({ type: 'node', id: '1' }, 'mother', { + type: 'node', + id: '1' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship 'mother' for type 'node' is not defined in schema` + } + ] + ) + ); }); test('replaceRelatedRecord with a related record with an invalid type for a non-polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecord({ type: 'node', id: '1' }, 'parent', { - type: 'person', - id: '1' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecord({ type: 'node', id: '1' }, 'parent', { + type: 'person', + id: '1' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship data is invalid\n - relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); test('replaceRelatedRecord with a related record with an invalid type for a polymorphic relationship', (assert) => { - assert.throws(() => { - cache.patch((t) => - t.replaceRelatedRecord({ type: 'person', id: '1' }, 'favoritePet', { - type: 'person', - id: '2' - }) - ); - }, new Error('Validation isssues encountered while building a transform operation')); + assert.throws( + () => { + cache.update((t) => + t.replaceRelatedRecord({ type: 'person', id: '1' }, 'favoritePet', { + type: 'person', + id: '2' + }) + ); + }, + new ValidationError( + 'Validation isssues encountered while building a transform operation', + [ + { + validator: StandardRecordValidators.RecordOperation, + validation: 'operationValid', + description: `record operation is invalid\n- relationship data is invalid\n - relatedRecord has a type 'person' which is not an allowed type for this relationship` + } + ] + ) + ); }); }); diff --git a/packages/@orbit/records/src/record-exceptions.ts b/packages/@orbit/records/src/record-exceptions.ts index 4ca34452..01f68e87 100644 --- a/packages/@orbit/records/src/record-exceptions.ts +++ b/packages/@orbit/records/src/record-exceptions.ts @@ -1,5 +1,8 @@ import { Exception } from '@orbit/core'; -import { ValidationIssue } from '@orbit/validators'; +import { + formatValidationDescription, + ValidationIssue +} from '@orbit/validators'; /** * An error occured related to the schema. @@ -17,12 +20,10 @@ export class SchemaError extends Exception { * A validation failed. */ export class ValidationError extends Exception { - public description: string; public issues?: ValidationIssue[]; constructor(description: string, issues?: ValidationIssue[]) { - super(description); - this.description = description; + super(formatValidationDescription(description, issues)); this.issues = issues; } } diff --git a/packages/@orbit/records/src/record-validators/record-operation-validator.ts b/packages/@orbit/records/src/record-validators/record-operation-validator.ts index e2a06d6f..08e42da8 100644 --- a/packages/@orbit/records/src/record-validators/record-operation-validator.ts +++ b/packages/@orbit/records/src/record-validators/record-operation-validator.ts @@ -1,5 +1,6 @@ import { Assertion } from '@orbit/core'; import { + formatValidationDescription, StandardValidator, ValidationIssue, Validator, @@ -159,7 +160,10 @@ export const validateRecordOperation: RecordOperationValidator = ( validation: 'operationValid', ref: operation, details: issues, - description: 'record operation is invalid' + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ]; } diff --git a/packages/@orbit/records/src/record-validators/record-query-expression-validator.ts b/packages/@orbit/records/src/record-validators/record-query-expression-validator.ts index eccaf459..4153f0ad 100644 --- a/packages/@orbit/records/src/record-validators/record-query-expression-validator.ts +++ b/packages/@orbit/records/src/record-validators/record-query-expression-validator.ts @@ -1,5 +1,6 @@ import { Assertion } from '@orbit/core'; import { + formatValidationDescription, StandardValidator, ValidationIssue, Validator, @@ -242,7 +243,10 @@ export const validateRecordQueryExpression: RecordQueryExpressionValidator = ( validation: 'queryExpressionValid', ref: expression, details: issues, - description: 'record query expression is invalid' + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ]; } diff --git a/packages/@orbit/records/src/record-validators/record-relationship-validator.ts b/packages/@orbit/records/src/record-validators/record-relationship-validator.ts index 946b1b99..01642116 100644 --- a/packages/@orbit/records/src/record-validators/record-relationship-validator.ts +++ b/packages/@orbit/records/src/record-validators/record-relationship-validator.ts @@ -1,6 +1,7 @@ import { Assertion } from '@orbit/core'; import { ArrayValidator, + formatValidationDescription, StandardValidator, StandardValidators, ValidationIssue, @@ -217,8 +218,11 @@ export const validateRecordRelationship: RecordRelationshipValidator = ( data }, validation: 'dataValid', - description: 'relationship data is invalid', - details: dataIssues + details: dataIssues, + description: formatValidationDescription( + 'relationship data is invalid', + dataIssues + ) } ]; } diff --git a/packages/@orbit/records/src/record-validators/related-record-validator.ts b/packages/@orbit/records/src/record-validators/related-record-validator.ts index 7e861342..a767a6df 100644 --- a/packages/@orbit/records/src/record-validators/related-record-validator.ts +++ b/packages/@orbit/records/src/record-validators/related-record-validator.ts @@ -1,5 +1,6 @@ import { Assertion } from '@orbit/core'; import { + formatValidationDescription, StandardValidator, ValidationIssue, Validator, @@ -110,7 +111,10 @@ export const validateRelatedRecord: RelatedRecordValidator = ( relatedRecord }, details: relatedRecordIssues, - description: 'relatedRecord is not a valid record identity' + description: formatValidationDescription( + 'relatedRecord is not a valid record identity', + relatedRecordIssues + ) } ]; } diff --git a/packages/@orbit/records/test/record-validators/record-operation-validator-test.ts b/packages/@orbit/records/test/record-validators/record-operation-validator-test.ts index 4fd82080..66c4a6ae 100644 --- a/packages/@orbit/records/test/record-validators/record-operation-validator-test.ts +++ b/packages/@orbit/records/test/record-validators/record-operation-validator-test.ts @@ -2,7 +2,16 @@ import { RecordSchema } from '../../src/record-schema'; import { StandardRecordValidators } from '../../src/record-validators/standard-record-validators'; import { buildRecordValidatorFor } from '../../src/record-validators/record-validator-builder'; import { validateRecordOperation } from '../../src/record-validators/record-operation-validator'; -import { RecordOperation } from '../../src'; +import { + RecordAttributeValidationIssue, + RecordFieldDefinitionIssue, + RecordKeyValidationIssue, + RecordOperation, + RecordRelationshipValidationIssue, + RecordTypeValidationIssue, + RelatedRecordValidationIssue +} from '../../src'; +import { formatValidationDescription } from '@orbit/validators'; const { module, test } = QUnit; @@ -105,6 +114,39 @@ module('validateRecordOperation', function (hooks) { } }; + const issues: RecordFieldDefinitionIssue[] = [ + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'key', + type: 'moon', + field: 'fakeKey' + }, + description: `key 'fakeKey' for type 'moon' is not defined in schema` + }, + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'attribute', + type: 'moon', + field: 'fakeAttr' + }, + description: `attribute 'fakeAttr' for type 'moon' is not defined in schema` + }, + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'relationship', + type: 'moon', + field: 'fakeRel' + }, + description: `relationship 'fakeRel' for type 'moon' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordOperation( { @@ -124,39 +166,11 @@ module('validateRecordOperation', function (hooks) { op: 'addRecord', record: invalidRecord }, - details: [ - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'key', - type: 'moon', - field: 'fakeKey' - }, - description: `key 'fakeKey' for type 'moon' is not defined in schema` - }, - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'attribute', - type: 'moon', - field: 'fakeAttr' - }, - description: `attribute 'fakeAttr' for type 'moon' is not defined in schema` - }, - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'relationship', - type: 'moon', - field: 'fakeRel' - }, - description: `relationship 'fakeRel' for type 'moon' is not defined in schema` - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -194,6 +208,19 @@ module('validateRecordOperation', function (hooks) { } }; + const issues: RecordFieldDefinitionIssue[] = [ + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'attribute', + type: 'moon', + field: 'fakeAttr' + }, + description: `attribute 'fakeAttr' for type 'moon' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordOperation( { @@ -213,19 +240,11 @@ module('validateRecordOperation', function (hooks) { op: 'updateRecord', record: invalidUpdate }, - details: [ - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'attribute', - type: 'moon', - field: 'fakeAttr' - }, - description: `attribute 'fakeAttr' for type 'moon' is not defined in schema` - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -258,6 +277,15 @@ module('validateRecordOperation', function (hooks) { id: '1' }; + const issues: RecordTypeValidationIssue[] = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'fake', + description: `Record type 'fake' does not exist in schema` + } + ]; + assert.deepEqual( validateRecordOperation( { @@ -277,15 +305,11 @@ module('validateRecordOperation', function (hooks) { op: 'removeRecord', record: invalidRecord }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'fake', - description: `Record type 'fake' does not exist in schema` - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -320,6 +344,19 @@ module('validateRecordOperation', function (hooks) { id: '1' }; + const issues: RecordKeyValidationIssue[] = [ + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'key', + type: 'fake', + field: 'remoteId' + }, + description: `key 'remoteId' for type 'fake' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordOperation( { @@ -343,19 +380,11 @@ module('validateRecordOperation', function (hooks) { key: 'remoteId', value: 'a' }, - details: [ - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'key', - type: 'fake', - field: 'remoteId' - }, - description: `key 'remoteId' for type 'fake' is not defined in schema` - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -390,6 +419,19 @@ module('validateRecordOperation', function (hooks) { id: '1' }; + const issues: RecordAttributeValidationIssue[] = [ + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + kind: 'attribute', + type: 'fake', + field: 'name' + }, + description: `attribute 'name' for type 'fake' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordOperation( { @@ -413,19 +455,11 @@ module('validateRecordOperation', function (hooks) { attribute: 'name', value: 'a' }, - details: [ - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - kind: 'attribute', - type: 'fake', - field: 'name' - }, - description: `attribute 'name' for type 'fake' is not defined in schema` - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -469,6 +503,23 @@ module('validateRecordOperation', function (hooks) { } }; + const issues: RelatedRecordValidationIssue[] = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'planet', id: '1' }, + relationship: 'moons', + relatedRecord: { type: 'planet', id: '1' } + }, + details: { + allowedTypes: ['moon'] + }, + description: + "relatedRecord has a type 'planet' which is not an allowed type for this relationship" + } + ]; + assert.deepEqual( validateRecordOperation(invalidOperation, { schema, @@ -479,23 +530,11 @@ module('validateRecordOperation', function (hooks) { validator: StandardRecordValidators.RecordOperation, validation: 'operationValid', ref: invalidOperation, - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'planet', id: '1' }, - relationship: 'moons', - relatedRecord: { type: 'planet', id: '1' } - }, - details: { - allowedTypes: ['moon'] - }, - description: - "relatedRecord has a type 'planet' which is not an allowed type for this relationship" - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -539,6 +578,23 @@ module('validateRecordOperation', function (hooks) { } }; + const issues: RelatedRecordValidationIssue[] = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'planet', id: '1' }, + relationship: 'moons', + relatedRecord: { type: 'planet', id: '1' } + }, + details: { + allowedTypes: ['moon'] + }, + description: + "relatedRecord has a type 'planet' which is not an allowed type for this relationship" + } + ]; + assert.deepEqual( validateRecordOperation(invalidOperation, { schema, @@ -549,23 +605,11 @@ module('validateRecordOperation', function (hooks) { validator: StandardRecordValidators.RecordOperation, validation: 'operationValid', ref: invalidOperation, - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'planet', id: '1' }, - relationship: 'moons', - relatedRecord: { type: 'planet', id: '1' } - }, - details: { - allowedTypes: ['moon'] - }, - description: - "relatedRecord has a type 'planet' which is not an allowed type for this relationship" - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -613,6 +657,40 @@ module('validateRecordOperation', function (hooks) { ] }; + const relationshipDataIssues: RelatedRecordValidationIssue[] = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'planet', id: '1' }, + relationship: 'moons', + relatedRecord: { type: 'planet', id: '1' } + }, + details: { + allowedTypes: ['moon'] + }, + description: + "relatedRecord has a type 'planet' which is not an allowed type for this relationship" + } + ]; + + const issues: RecordRelationshipValidationIssue[] = [ + { + validator: StandardRecordValidators.RecordRelationship, + validation: 'dataValid', + ref: { + record: { type: 'planet', id: '1' }, + relationship: 'moons', + data: [{ type: 'planet', id: '1' }] + }, + details: relationshipDataIssues, + description: formatValidationDescription( + 'relationship data is invalid', + relationshipDataIssues + ) + } + ]; + assert.deepEqual( validateRecordOperation(invalidOperation, { schema, @@ -623,35 +701,11 @@ module('validateRecordOperation', function (hooks) { validator: StandardRecordValidators.RecordOperation, validation: 'operationValid', ref: invalidOperation, - details: [ - { - validator: StandardRecordValidators.RecordRelationship, - validation: 'dataValid', - ref: { - record: { type: 'planet', id: '1' }, - relationship: 'moons', - data: [{ type: 'planet', id: '1' }] - }, - description: 'relationship data is invalid', - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'planet', id: '1' }, - relationship: 'moons', - relatedRecord: { type: 'planet', id: '1' } - }, - details: { - allowedTypes: ['moon'] - }, - description: - "relatedRecord has a type 'planet' which is not an allowed type for this relationship" - } - ] - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' @@ -695,6 +749,40 @@ module('validateRecordOperation', function (hooks) { } }; + const relationshipDataIssues: RelatedRecordValidationIssue[] = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'moon', id: '1' }, + relationship: 'planet', + relatedRecord: { type: 'moon', id: '1' } + }, + details: { + allowedTypes: ['planet'] + }, + description: + "relatedRecord has a type 'moon' which is not an allowed type for this relationship" + } + ]; + + const issues: RecordRelationshipValidationIssue[] = [ + { + validator: StandardRecordValidators.RecordRelationship, + validation: 'dataValid', + ref: { + record: { type: 'moon', id: '1' }, + relationship: 'planet', + data: { type: 'moon', id: '1' } + }, + details: relationshipDataIssues, + description: formatValidationDescription( + 'relationship data is invalid', + relationshipDataIssues + ) + } + ]; + assert.deepEqual( validateRecordOperation(invalidOperation, { schema, @@ -705,35 +793,11 @@ module('validateRecordOperation', function (hooks) { validator: StandardRecordValidators.RecordOperation, validation: 'operationValid', ref: invalidOperation, - details: [ - { - validator: StandardRecordValidators.RecordRelationship, - validation: 'dataValid', - ref: { - record: { type: 'moon', id: '1' }, - relationship: 'planet', - data: { type: 'moon', id: '1' } - }, - description: 'relationship data is invalid', - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'moon', id: '1' }, - relationship: 'planet', - relatedRecord: { type: 'moon', id: '1' } - }, - details: { - allowedTypes: ['planet'] - }, - description: - "relatedRecord has a type 'moon' which is not an allowed type for this relationship" - } - ] - } - ], - description: 'record operation is invalid' + details: issues, + description: formatValidationDescription( + 'record operation is invalid', + issues + ) } ], 'invalid operation' diff --git a/packages/@orbit/records/test/record-validators/record-query-expression-validator-test.ts b/packages/@orbit/records/test/record-validators/record-query-expression-validator-test.ts index 37cf5b38..a793bae3 100644 --- a/packages/@orbit/records/test/record-validators/record-query-expression-validator-test.ts +++ b/packages/@orbit/records/test/record-validators/record-query-expression-validator-test.ts @@ -3,6 +3,7 @@ import { StandardRecordValidators } from '../../src/record-validators/standard-r import { buildRecordValidatorFor } from '../../src/record-validators/record-validator-builder'; import { validateRecordQueryExpression } from '../../src/record-validators/record-query-expression-validator'; import { RecordQueryExpression } from '../../src'; +import { formatValidationDescription } from '@orbit/validators'; const { module, test } = QUnit; @@ -90,6 +91,15 @@ module('validateRecordQueryExpression', function (hooks) { id: '1' }; + const issues = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'unknown', + description: `Record type 'unknown' does not exist in schema` + } + ]; + assert.deepEqual( validateRecordQueryExpression( { @@ -109,15 +119,11 @@ module('validateRecordQueryExpression', function (hooks) { op: 'findRecord', record: invalidRecord }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'unknown', - description: `Record type 'unknown' does not exist in schema` - } - ], - description: 'record query expression is invalid' + details: issues, + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ], 'invalid query expression' @@ -151,6 +157,25 @@ module('validateRecordQueryExpression', function (hooks) { id: '1' }; + const issues = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'unknown', + description: `Record type 'unknown' does not exist in schema` + }, + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + field: 'planet', + kind: 'relationship', + type: 'unknown' + }, + description: `relationship 'planet' for type 'unknown' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordQueryExpression( { @@ -172,25 +197,11 @@ module('validateRecordQueryExpression', function (hooks) { record: invalidRecord, relationship: 'planet' }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'unknown', - description: `Record type 'unknown' does not exist in schema` - }, - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - field: 'planet', - kind: 'relationship', - type: 'unknown' - }, - description: `relationship 'planet' for type 'unknown' is not defined in schema` - } - ], - description: 'record query expression is invalid' + details: issues, + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ], 'invalid query expression' @@ -224,6 +235,25 @@ module('validateRecordQueryExpression', function (hooks) { id: '1' }; + const issues = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'unknown', + description: `Record type 'unknown' does not exist in schema` + }, + { + validator: StandardRecordValidators.RecordFieldDefinition, + validation: 'fieldDefined', + ref: { + field: 'moons', + kind: 'relationship', + type: 'unknown' + }, + description: `relationship 'moons' for type 'unknown' is not defined in schema` + } + ]; + assert.deepEqual( validateRecordQueryExpression( { @@ -245,25 +275,11 @@ module('validateRecordQueryExpression', function (hooks) { record: invalidRecord, relationship: 'moons' }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'unknown', - description: `Record type 'unknown' does not exist in schema` - }, - { - validator: StandardRecordValidators.RecordFieldDefinition, - validation: 'fieldDefined', - ref: { - field: 'moons', - kind: 'relationship', - type: 'unknown' - }, - description: `relationship 'moons' for type 'unknown' is not defined in schema` - } - ], - description: 'record query expression is invalid' + details: issues, + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ], 'invalid query expression' @@ -286,6 +302,15 @@ module('validateRecordQueryExpression', function (hooks) { 'valid query expression' ); + const issues = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'unknown', + description: `Record type 'unknown' does not exist in schema` + } + ]; + assert.deepEqual( validateRecordQueryExpression( { @@ -305,15 +330,11 @@ module('validateRecordQueryExpression', function (hooks) { op: 'findRecords', type: 'unknown' }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'unknown', - description: `Record type 'unknown' does not exist in schema` - } - ], - description: 'record query expression is invalid' + details: issues, + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ], 'invalid query expression' @@ -346,6 +367,15 @@ module('validateRecordQueryExpression', function (hooks) { id: '1' }; + const issues = [ + { + validator: StandardRecordValidators.RecordType, + validation: 'recordTypeDefined', + ref: 'unknown', + description: `Record type 'unknown' does not exist in schema` + } + ]; + assert.deepEqual( validateRecordQueryExpression( { @@ -365,15 +395,11 @@ module('validateRecordQueryExpression', function (hooks) { op: 'findRecords', records: [invalidRecord] }, - details: [ - { - validator: StandardRecordValidators.RecordType, - validation: 'recordTypeDefined', - ref: 'unknown', - description: `Record type 'unknown' does not exist in schema` - } - ], - description: 'record query expression is invalid' + details: issues, + description: formatValidationDescription( + 'record query expression is invalid', + issues + ) } ], 'invalid query expression' diff --git a/packages/@orbit/records/test/record-validators/record-relationship-validator-test.ts b/packages/@orbit/records/test/record-validators/record-relationship-validator-test.ts index 69d95ad7..3f360f93 100644 --- a/packages/@orbit/records/test/record-validators/record-relationship-validator-test.ts +++ b/packages/@orbit/records/test/record-validators/record-relationship-validator-test.ts @@ -2,6 +2,7 @@ import { RecordSchema } from '../../src/record-schema'; import { StandardRecordValidators } from '../../src/record-validators/standard-record-validators'; import { buildRecordValidatorFor } from '../../src/record-validators/record-validator-builder'; import { validateRecordRelationship } from '../../src/record-validators/record-relationship-validator'; +import { formatValidationDescription } from '@orbit/validators'; const { module, test } = QUnit; @@ -168,6 +169,32 @@ module('validateRecordRelationship', function (hooks) { test('will check that records in data are valid record identities', function (assert) { const relationshipDef = schema.getRelationship('moon', 'planet'); + const relationshipDataIssues = [ + { + description: "Record type 'fake' does not exist in schema", + ref: 'fake', + validation: 'recordTypeDefined', + validator: 'recordType' + } + ]; + + const issues = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordValid', + ref: { + record: { type: 'moon', id: 'm1' }, + relationship: 'planet', + relatedRecord: { type: 'fake', id: 'p1' } + }, + details: relationshipDataIssues, + description: formatValidationDescription( + 'relatedRecord is not a valid record identity', + relationshipDataIssues + ) + } + ]; + assert.deepEqual( validateRecordRelationship( { @@ -190,27 +217,11 @@ module('validateRecordRelationship', function (hooks) { relationship: 'planet', data: { type: 'fake', id: 'p1' } }, - description: 'relationship data is invalid', - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordValid', - ref: { - record: { type: 'moon', id: 'm1' }, - relationship: 'planet', - relatedRecord: { type: 'fake', id: 'p1' } - }, - description: 'relatedRecord is not a valid record identity', - details: [ - { - description: "Record type 'fake' does not exist in schema", - ref: 'fake', - validation: 'recordTypeDefined', - validator: 'recordType' - } - ] - } - ] + details: issues, + description: formatValidationDescription( + 'relationship data is invalid', + issues + ) } ] ); @@ -235,6 +246,23 @@ module('validateRecordRelationship', function (hooks) { undefined ); + const issues = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'moon', id: 'm1' }, + relationship: 'planet', + relatedRecord: { type: 'solarSystem', id: 'ss1' } + }, + details: { + allowedTypes: ['planet'] + }, + description: + "relatedRecord has a type 'solarSystem' which is not an allowed type for this relationship" + } + ]; + assert.deepEqual( validateRecordRelationship( { @@ -257,23 +285,11 @@ module('validateRecordRelationship', function (hooks) { relationship: 'planet', data: { type: 'solarSystem', id: 'ss1' } }, - description: 'relationship data is invalid', - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'moon', id: 'm1' }, - relationship: 'planet', - relatedRecord: { type: 'solarSystem', id: 'ss1' } - }, - details: { - allowedTypes: ['planet'] - }, - description: - "relatedRecord has a type 'solarSystem' which is not an allowed type for this relationship" - } - ] + details: issues, + description: formatValidationDescription( + 'relationship data is invalid', + issues + ) } ] ); @@ -298,6 +314,23 @@ module('validateRecordRelationship', function (hooks) { undefined ); + const issues = [ + { + validator: StandardRecordValidators.RelatedRecord, + validation: 'relatedRecordType', + ref: { + record: { type: 'solarSystem', id: 'ss1' }, + relationship: 'objects', + relatedRecord: { type: 'solarSystem', id: 'ss1' } + }, + details: { + allowedTypes: ['planet', 'moon'] + }, + description: + "relatedRecord has a type 'solarSystem' which is not an allowed type for this relationship" + } + ]; + assert.deepEqual( validateRecordRelationship( { @@ -320,23 +353,11 @@ module('validateRecordRelationship', function (hooks) { relationship: 'objects', data: [{ type: 'solarSystem', id: 'ss1' }] }, - description: 'relationship data is invalid', - details: [ - { - validator: StandardRecordValidators.RelatedRecord, - validation: 'relatedRecordType', - ref: { - record: { type: 'solarSystem', id: 'ss1' }, - relationship: 'objects', - relatedRecord: { type: 'solarSystem', id: 'ss1' } - }, - details: { - allowedTypes: ['planet', 'moon'] - }, - description: - "relatedRecord has a type 'solarSystem' which is not an allowed type for this relationship" - } - ] + details: issues, + description: formatValidationDescription( + 'relationship data is invalid', + issues + ) } ] ); @@ -376,6 +397,18 @@ module('validateRecordRelationship', function (hooks) { test('will check array validations like minItems for hasMany relationships', function (assert) { const relationshipDef = schema.getRelationship('solarSystem', 'objects'); + const issues = [ + { + validator: 'array', + validation: 'minItems', + ref: [], + details: { + minItems: 1 + }, + description: 'has too few members' + } + ]; + assert.deepEqual( validateRecordRelationship( { @@ -398,18 +431,11 @@ module('validateRecordRelationship', function (hooks) { relationship: 'objects', data: [] }, - description: 'relationship data is invalid', - details: [ - { - validator: 'array', - validation: 'minItems', - ref: [], - details: { - minItems: 1 - }, - description: 'has too few members' - } - ] + details: issues, + description: formatValidationDescription( + 'relationship data is invalid', + issues + ) } ] ); diff --git a/packages/@orbit/records/test/record-validators/related-record-validator-test.ts b/packages/@orbit/records/test/record-validators/related-record-validator-test.ts index 25e9e4bb..0716dc5e 100644 --- a/packages/@orbit/records/test/record-validators/related-record-validator-test.ts +++ b/packages/@orbit/records/test/record-validators/related-record-validator-test.ts @@ -2,6 +2,7 @@ import { RecordSchema } from '../../src/record-schema'; import { StandardRecordValidators } from '../../src/record-validators/standard-record-validators'; import { buildRecordValidatorFor } from '../../src/record-validators/record-validator-builder'; import { validateRelatedRecord } from '../../src/record-validators/related-record-validator'; +import { formatValidationDescription } from '@orbit/validators'; const { module, test } = QUnit; @@ -160,6 +161,15 @@ module('validateRelatedRecord', function (hooks) { test('will check if relatedRecord is a valid record identity', function (assert) { const relationshipDef = schema.getRelationship('moon', 'planet'); + const issues = [ + { + description: "Record type 'fake' does not exist in schema", + ref: 'fake', + validation: 'recordTypeDefined', + validator: 'recordType' + } + ]; + assert.deepEqual( validateRelatedRecord( { @@ -182,15 +192,11 @@ module('validateRelatedRecord', function (hooks) { relationship: 'planet', relatedRecord: { type: 'fake', id: 'p1' } }, - details: [ - { - description: "Record type 'fake' does not exist in schema", - ref: 'fake', - validation: 'recordTypeDefined', - validator: 'recordType' - } - ], - description: 'relatedRecord is not a valid record identity' + description: formatValidationDescription( + 'relatedRecord is not a valid record identity', + issues + ), + details: issues } ] ); diff --git a/packages/@orbit/validators/src/validator.ts b/packages/@orbit/validators/src/validator.ts index 1842b22e..8359532b 100644 --- a/packages/@orbit/validators/src/validator.ts +++ b/packages/@orbit/validators/src/validator.ts @@ -40,3 +40,16 @@ export type Validator< Options = ValidationOptions, Issue = ValidationIssue > = (input: Input, options?: Options) => undefined | Issue[]; + +export function formatValidationDescription( + summary: string, + issues?: ValidationIssue[] +): string { + if (issues && issues.length > 0) { + return `${summary}\n${issues + .map((i) => `- ${i.description.replace(/\n/g, '\n ')}`) + .join('\n')}`; + } else { + return summary; + } +} diff --git a/packages/@orbit/validators/test/validator-test.ts b/packages/@orbit/validators/test/validator-test.ts new file mode 100644 index 00000000..5db28c3d --- /dev/null +++ b/packages/@orbit/validators/test/validator-test.ts @@ -0,0 +1,57 @@ +import { formatValidationDescription } from '../src/validator'; + +const { module, test } = QUnit; + +/////////////////////////////////////////////////////////////////////////////// + +module('formatValidationDescription', function (hooks) { + test('includes summary only if no issues are included', function (assert) { + assert.strictEqual(formatValidationDescription('summary', []), 'summary'); + }); + + test('includes summary followed by issue descriptions', function (assert) { + assert.strictEqual( + formatValidationDescription('summary', [ + { + validator: 'v1', + validation: 'v1-a', + description: 'a' + }, + { + validator: 'v2', + validation: 'v2-a', + description: 'b' + }, + { + validator: 'v3', + validation: 'v3-a', + description: 'c' + } + ]), + `summary\n- a\n- b\n- c` + ); + }); + + test('nests any new lines in issue descriptions', function (assert) { + assert.strictEqual( + formatValidationDescription('summary', [ + { + validator: 'v1', + validation: 'v1-a', + description: 'a\n- a1\n- a2' + }, + { + validator: 'v2', + validation: 'v2-a', + description: 'b' + }, + { + validator: 'v3', + validation: 'v3-a', + description: 'c' + } + ]), + `summary\n- a\n - a1\n - a2\n- b\n- c` + ); + }); +});