diff --git a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts index b64b43de0a8..3464246ddea 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts @@ -6,306 +6,589 @@ import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; import { InvalidError } from '@ember-data/adapter/error'; +import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; +import { DSModel } from '@ember-data/types/q/ds-model'; +import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { NewRecordIdentifier, RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; -import type { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; +import type { ChangedAttributesHash, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import { Dict } from '@ember-data/types/q/utils'; -class Person extends Model { - // TODO fix the typing for naked attrs - @attr('string', {}) - name; +if (V2CACHE_SINGLETON_MANAGER) { + class Person extends Model { + @attr declare firstName: string; + @attr declare lastName: string; + } - @attr('string', {}) - lastName; -} + class TestRecordData implements RecordData { + version: '2' = '2'; -class TestRecordIdentifier implements NewRecordIdentifier { - constructor(public id: string | null, public lid: string, public type: string) {} -} + _errors?: JsonApiValidationError[]; + _isNew: boolean = false; -class TestRecordData implements RecordDataV1 { - setIsDeleted(isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - version?: '1' | undefined = '1'; - isDeletionCommitted(): boolean { - return false; - } - id: string | null = '1'; - clientId: string | null = 'test-record-data-1'; - modelName = 'tst'; + pushData( + identifier: StableRecordIdentifier, + data: JsonApiResource, + calculateChanges?: boolean | undefined + ): void | string[] {} + clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { + this._isNew = true; + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void {} + commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[] | undefined): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): SingleResourceRelationship | CollectionResourceRelationship { + throw new Error('Method not implemented.'); + } + setBelongsTo(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier | null): void { + throw new Error('Method not implemented.'); + } + setHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + addToHasMany( + identifier: StableRecordIdentifier, + propertyName: string, + value: StableRecordIdentifier[], + idx?: number | undefined + ): void { + throw new Error('Method not implemented.'); + } + removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } - getResourceIdentifier() { - if (this.clientId !== null) { - return new TestRecordIdentifier(this.id, this.clientId, this.modelName); + getErrors(identifier: StableRecordIdentifier): JsonApiValidationError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; } } - _errors: JsonApiValidationError[] = []; - getErrors(recordIdentifier: RecordIdentifier): JsonApiValidationError[] { - return this._errors; - } - commitWasRejected(identifier: StableRecordIdentifier, errors: JsonApiValidationError[]): void { - this._errors = errors; - } + module('integration/record-data Custom RecordData (v2) Errors', function (hooks) { + setupTest(hooks); - // Use correct interface once imports have been fix - _storeWrapper: any; + test('RecordData Invalid Errors', async function (assert) { + assert.expect(3); - pushData(data: object, calculateChange: true): string[]; - pushData(data: object, calculateChange?: false): void; - pushData(data: object, calculateChange?: boolean): string[] | void {} + const { owner } = this; - clientDidCreate() {} + class LifecycleRecordData extends TestRecordData { + commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[]) { + super.commitWasRejected(identifier, errors); + assert.strictEqual(errors?.[0]?.detail, 'is a generally unsavoury character', 'received the error'); + assert.strictEqual(errors?.[0]?.source.pointer, '/data/attributes/name', 'pointer is correct'); + } + } + class TestStore extends Store { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + return new LifecycleRecordData(); + } + } + class TestAdapter extends EmberObject { + updateRecord() { + return Promise.reject( + new InvalidError([ + { + title: 'Invalid Attribute', + detail: 'is a generally unsavoury character', + source: { + pointer: '/data/attributes/name', + }, + }, + ]) + ); + } - willCommit() {} + createRecord() { + return Promise.resolve(); + } + } - unloadRecord() {} - rollbackAttributes() { - return []; - } - changedAttributes(): any {} + owner.register('model:person', Person); + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter); - hasChangedAttributes(): boolean { - return false; - } + const store = owner.lookup('service:store') as Store; - setDirtyAttribute(key: string, value: any) {} + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', + }, + }, + }); - getAttr(key: string): string { - return 'test'; - } + try { + await (person as DSModel).save(); + assert.ok(false, 'we should error'); + } catch (error) { + assert.ok(true, 'we erred'); + } + }); - getHasMany(key: string) { - return {}; - } + test('RecordData Network Errors', async function (assert) { + assert.expect(2); - isRecordInUse(): boolean { - return true; - } + const { owner } = this; - isNew() { - return false; - } + class LifecycleRecordData extends TestRecordData { + commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[]) { + super.commitWasRejected(identifier, errors); + assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); + } + } + class TestStore extends Store { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + return new LifecycleRecordData(); + } + } + class TestAdapter extends EmberObject { + updateRecord() { + return Promise.reject(); + } + + createRecord() { + return Promise.resolve(); + } + } - isDeleted() { - return false; - } + owner.register('model:person', Person); + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter); - addToHasMany(key: string, recordDatas: RecordData[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: RecordData[]) {} - setDirtyHasMany(key: string, recordDatas: RecordData[]) {} + const store = owner.lookup('service:store') as Store; - getBelongsTo(key: string) { - return {}; - } + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', + }, + }, + }); - setDirtyBelongsTo(name: string, recordData: RecordData | null) {} + try { + await (person as DSModel).save(); + assert.ok(false, 'we should error'); + } catch (error) { + assert.ok(true, 'we erred'); + } + }); - didCommit(data) {} + test('RecordData Invalid Errors Can Be Reflected On The Record', async function (assert) { + const { owner } = this; + let errorsToReturn: JsonApiValidationError[] | undefined; + let storeWrapper; - _initRecordCreateOptions(options) { - return {}; - } -} + class LifecycleRecordData extends TestRecordData { + getErrors(): JsonApiValidationError[] { + return errorsToReturn || []; + } + } -let CustomStore = Store.extend({ - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { - return new TestRecordData(); - }, -}); + class TestStore extends Store { + createRecordDataFor(identifier: StableRecordIdentifier, sw: RecordDataStoreWrapper): RecordData { + storeWrapper = sw; + return new LifecycleRecordData(); + } + } -module('integration/record-data - Custom RecordData Errors', function (hooks) { - setupTest(hooks); + owner.register('model:person', Person); + owner.register('service:store', TestStore); - let store; + const store = owner.lookup('service:store') as Store; - hooks.beforeEach(function () { - let { owner } = this; + const person: DSModel = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Tom', + lastName: 'Dale', + }, + }, + }) as DSModel; + + const identifier = recordIdentifierFor(person); + let nameError = person.errors.errorsFor('firstName').firstObject; + assert.strictEqual(nameError, undefined, 'no error shows up on firstName initially'); + assert.true(person.isValid, 'person is initially valid'); + + errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/firstName', + }, + }, + ]; + storeWrapper.notifyChange(identifier, 'errors'); + + nameError = person.errors.errorsFor('firstName').firstObject; + assert.strictEqual(nameError?.attribute, 'firstName', 'error shows up on name'); + assert.false(person.isValid, 'person is not valid'); + + errorsToReturn = []; + storeWrapper.notifyChange(identifier, 'errors'); + + assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on name'); + assert.true(person.isValid, 'person is valid'); + + errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/lastName', + }, + }, + ]; + storeWrapper.notifyChange(identifier, 'errors'); - owner.register('model:person', Person); - owner.unregister('service:store'); - owner.register('service:store', CustomStore); - owner.register('serializer:application', JSONAPISerializer); + assert.false(person.isValid, 'person is not valid'); + assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on firstName'); + let lastNameError = person.errors.errorsFor('lastName').firstObject; + assert.strictEqual(lastNameError?.attribute, 'lastName', 'error shows up on lastName'); + }); }); +} else { + module('integration/record-data - Custom RecordData (v1) Errors', function (hooks) { + setupTest(hooks); - test('Record Data invalid errors', async function (assert) { - assert.expect(2); + let store; - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - - class LifecycleRecordData extends TestRecordData { - commitWasRejected(recordIdentifier, errors) { - super.commitWasRejected(recordIdentifier, errors); - assert.strictEqual(errors[0].detail, 'is a generally unsavoury character', 'received the error'); - assert.strictEqual(errors[0].source.pointer, '/data/attributes/name', 'pointer is correct'); - } + class Person extends Model { + // TODO fix the typing for naked attrs + @attr('string', {}) + name; + + @attr('string', {}) + lastName; } - let TestStore = Store.extend({ - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - return new LifecycleRecordData(); - }, - }); + class TestRecordIdentifier implements NewRecordIdentifier { + constructor(public id: string | null, public lid: string, public type: string) {} + } - let TestAdapter = EmberObject.extend({ - updateRecord() { - return Promise.reject( - new InvalidError([ - { - title: 'Invalid Attribute', - detail: 'is a generally unsavoury character', - source: { - pointer: '/data/attributes/name', - }, - }, - ]) - ); - }, + class TestRecordData implements RecordDataV1 { + setIsDeleted(isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + version?: '1' | undefined = '1'; + isDeletionCommitted(): boolean { + return false; + } + id: string | null = '1'; + clientId: string | null = 'test-record-data-1'; + modelName = 'tst'; + + getResourceIdentifier() { + if (this.clientId !== null) { + return new TestRecordIdentifier(this.id, this.clientId, this.modelName); + } + } - createRecord() { - return Promise.resolve(); - }, - }); + _errors: JsonApiValidationError[] = []; + getErrors(recordIdentifier: RecordIdentifier): JsonApiValidationError[] { + return this._errors; + } + commitWasRejected(identifier: StableRecordIdentifier, errors: JsonApiValidationError[]): void { + this._errors = errors; + } - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); + // Use correct interface once imports have been fix + _storeWrapper: any; - store = owner.lookup('service:store'); + pushData(data: object, calculateChange: true): string[]; + pushData(data: object, calculateChange?: false): void; + pushData(data: object, calculateChange?: boolean): string[] | void {} - store.push({ - data: [personHash], - }); - let person = store.peekRecord('person', '1'); - person.save().then( - () => {}, - (err) => {} - ); - }); + clientDidCreate() {} - test('Record Data adapter errors', async function (assert) { - assert.expect(1); - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; + willCommit() {} + + unloadRecord() {} + rollbackAttributes() { + return []; + } + changedAttributes(): any {} + + hasChangedAttributes(): boolean { + return false; + } + + setDirtyAttribute(key: string, value: any) {} + + getAttr(key: string): string { + return 'test'; + } + + getHasMany(key: string) { + return {}; + } + + isRecordInUse(): boolean { + return true; + } + + isNew() { + return false; + } + + isDeleted() { + return false; + } + + addToHasMany(key: string, recordDatas: RecordData[], idx?: number) {} + removeFromHasMany(key: string, recordDatas: RecordData[]) {} + setDirtyHasMany(key: string, recordDatas: RecordData[]) {} - class LifecycleRecordData extends TestRecordData { - commitWasRejected(recordIdentifier, errors) { - super.commitWasRejected(recordIdentifier, errors); - assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); + getBelongsTo(key: string) { + return {}; + } + + setDirtyBelongsTo(name: string, recordData: RecordData | null) {} + + didCommit(data) {} + + _initRecordCreateOptions(options) { + return {}; } } - let TestStore = Store.extend({ + let CustomStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { - return new LifecycleRecordData(); + return new TestRecordData(); }, }); - let TestAdapter = EmberObject.extend({ - updateRecord() { - return Promise.reject(); - }, + hooks.beforeEach(function () { + let { owner } = this; + + owner.register('model:person', Person); + owner.unregister('service:store'); + owner.register('service:store', CustomStore); + owner.register('serializer:application', JSONAPISerializer); }); - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); + test('Record Data invalid errors', async function (assert) { + assert.expect(2); + + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }; + let { owner } = this; + + class LifecycleRecordData extends TestRecordData { + commitWasRejected(recordIdentifier, errors) { + super.commitWasRejected(recordIdentifier, errors); + assert.strictEqual(errors[0].detail, 'is a generally unsavoury character', 'received the error'); + assert.strictEqual(errors[0].source.pointer, '/data/attributes/name', 'pointer is correct'); + } + } + + let TestStore = Store.extend({ + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + return new LifecycleRecordData(); + }, + }); + + let TestAdapter = EmberObject.extend({ + updateRecord() { + return Promise.reject( + new InvalidError([ + { + title: 'Invalid Attribute', + detail: 'is a generally unsavoury character', + source: { + pointer: '/data/attributes/name', + }, + }, + ]) + ); + }, + + createRecord() { + return Promise.resolve(); + }, + }); + + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter, { singleton: false }); - store = owner.lookup('service:store'); + store = owner.lookup('service:store'); - store.push({ - data: [personHash], + store.push({ + data: [personHash], + }); + let person = store.peekRecord('person', '1'); + person.save().then( + () => {}, + (err) => {} + ); }); - let person = store.peekRecord('person', '1'); - await person.save().then( - () => {}, - (err) => {} - ); - }); - test('Getting errors from Record Data shows up on the record', async function (assert) { - assert.expect(7); - let storeWrapper; - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - lastName: 'something', - }, - }; - let { owner } = this; - let errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/name', + test('Record Data adapter errors', async function (assert) { + assert.expect(1); + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', }, - }, - ]; - - class LifecycleRecordData extends TestRecordData { - constructor(sw) { - super(); - storeWrapper = sw; + }; + let { owner } = this; + + class LifecycleRecordData extends TestRecordData { + commitWasRejected(recordIdentifier, errors) { + super.commitWasRejected(recordIdentifier, errors); + assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); + } } - getErrors(recordIdentifier: RecordIdentifier): JsonApiValidationError[] { - return errorsToReturn; - } - } + let TestStore = Store.extend({ + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + return new LifecycleRecordData(); + }, + }); - let TestStore = Store.extend({ - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { - return new LifecycleRecordData(wrapper); - }, - }); + let TestAdapter = EmberObject.extend({ + updateRecord() { + return Promise.reject(); + }, + }); - owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter, { singleton: false }); - store.push({ - data: [personHash], + store = owner.lookup('service:store'); + + store.push({ + data: [personHash], + }); + let person = store.peekRecord('person', '1'); + await person.save().then( + () => {}, + (err) => {} + ); }); - let person = store.peekRecord('person', '1'); - const identifier = recordIdentifierFor(person); - let nameError = person.errors.errorsFor('name').firstObject; - assert.strictEqual(nameError.attribute, 'name', 'error shows up on name'); - assert.false(person.isValid, 'person is not valid'); - errorsToReturn = []; - storeWrapper.notifyChange(identifier, 'errors'); - assert.true(person.isValid, 'person is valid'); - assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); - errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/lastName', + + test('Getting errors from Record Data shows up on the record', async function (assert) { + assert.expect(7); + let storeWrapper; + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + lastName: 'something', }, - }, - ]; - storeWrapper.notifyChange(identifier, 'errors'); - assert.false(person.isValid, 'person is valid'); - assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); - let lastNameError = person.errors.errorsFor('lastName').firstObject; - assert.strictEqual(lastNameError.attribute, 'lastName', 'error shows up on lastName'); + }; + let { owner } = this; + let errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/name', + }, + }, + ]; + + class LifecycleRecordData extends TestRecordData { + constructor(sw) { + super(); + storeWrapper = sw; + } + + getErrors(recordIdentifier: RecordIdentifier): JsonApiValidationError[] { + return errorsToReturn; + } + } + + let TestStore = Store.extend({ + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + return new LifecycleRecordData(wrapper); + }, + }); + + owner.register('service:store', TestStore); + store = owner.lookup('service:store'); + + store.push({ + data: [personHash], + }); + let person = store.peekRecord('person', '1'); + const identifier = recordIdentifierFor(person); + let nameError = person.errors.errorsFor('name').firstObject; + assert.strictEqual(nameError.attribute, 'name', 'error shows up on name'); + assert.false(person.isValid, 'person is not valid'); + errorsToReturn = []; + storeWrapper.notifyChange(identifier, 'errors'); + assert.true(person.isValid, 'person is valid'); + assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); + errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/lastName', + }, + }, + ]; + storeWrapper.notifyChange(identifier, 'errors'); + assert.false(person.isValid, 'person is valid'); + assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); + let lastNameError = person.errors.errorsFor('lastName').firstObject; + assert.strictEqual(lastNameError.attribute, 'lastName', 'error shows up on lastName'); + }); }); -}); +} diff --git a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts index a4d391a3c0a..026b66b3ffa 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts @@ -6,13 +6,16 @@ import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; +import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; +import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { NewRecordIdentifier, RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; -import type { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; +import type { ChangedAttributesHash, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import { Dict } from '@ember-data/types/q/utils'; class Person extends Model { // TODO fix the typing for naked attrs @@ -27,7 +30,7 @@ class TestRecordIdentifier implements NewRecordIdentifier { constructor(public id: string | null, public lid: string, public type: string) {} } -class TestRecordData implements RecordDataV1 { +class V1TestRecordData implements RecordDataV1 { setIsDeleted(isDeleted: boolean): void { throw new Error('Method not implemented.'); } @@ -110,6 +113,86 @@ class TestRecordData implements RecordDataV1 { return {}; } } +class V2TestRecordData implements RecordData { + version: '2' = '2'; + + _errors?: JsonApiValidationError[]; + _isNew: boolean = false; + + pushData( + identifier: StableRecordIdentifier, + data: JsonApiResource, + calculateChanges?: boolean | undefined + ): void | string[] {} + clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { + this._isNew = true; + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void {} + commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[] | undefined): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): SingleResourceRelationship | CollectionResourceRelationship { + throw new Error('Method not implemented.'); + } + setBelongsTo(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier | null): void { + throw new Error('Method not implemented.'); + } + setHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + addToHasMany( + identifier: StableRecordIdentifier, + propertyName: string, + value: StableRecordIdentifier[], + idx?: number | undefined + ): void { + throw new Error('Method not implemented.'); + } + removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + + getErrors(identifier: StableRecordIdentifier): JsonApiValidationError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; + } +} +const TestRecordData = V2CACHE_SINGLETON_MANAGER ? V2TestRecordData : V1TestRecordData; const CustomStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { diff --git a/packages/-ember-data/tests/integration/record-data/record-data-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-test.ts index de5b7bb7ea4..848c394e177 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-test.ts @@ -7,12 +7,19 @@ import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; +import type { + CollectionResourceRelationship, + SingleResourceRelationship, +} from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; +import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data'; +import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { Dict } from '@ember-data/types/q/utils'; class Person extends Model { // TODO fix the typing for naked attrs @@ -33,10 +40,15 @@ class House extends Model { } // TODO: this should work -// class TestRecordData implements RecordData -class TestRecordData { - // Use correct interface once imports have been fix - _storeWrapper: any; +// class TestRecordData implements RecordDatav1 +class V1TestRecordData { + _storeWrapper: RecordDataStoreWrapper; + _identifier: StableRecordIdentifier; + + constructor(wrapper: RecordDataStoreWrapper, identifier: StableRecordIdentifier) { + this._storeWrapper = wrapper; + this._identifier = identifier; + } pushData(data: object, calculateChange: true): string[]; pushData(data: object, calculateChange?: false): void; @@ -95,9 +107,100 @@ class TestRecordData { } } +class V2TestRecordData implements RecordData { + version: '2' = '2'; + + _errors?: JsonApiValidationError[]; + _isNew: boolean = false; + _storeWrapper: RecordDataStoreWrapper; + _identifier: StableRecordIdentifier; + + constructor(wrapper: RecordDataStoreWrapper, identifier: StableRecordIdentifier) { + this._storeWrapper = wrapper; + this._identifier = identifier; + } + + pushData( + identifier: StableRecordIdentifier, + data: JsonApiResource, + calculateChanges?: boolean | undefined + ): void | string[] {} + clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { + this._isNew = true; + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void {} + commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[] | undefined): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + return []; + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): SingleResourceRelationship | CollectionResourceRelationship { + throw new Error('Method not implemented.'); + } + setBelongsTo(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier | null): void { + throw new Error('Method not implemented.'); + } + setHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + addToHasMany( + identifier: StableRecordIdentifier, + propertyName: string, + value: StableRecordIdentifier[], + idx?: number | undefined + ): void { + throw new Error('Method not implemented.'); + } + removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + + getErrors(identifier: StableRecordIdentifier): JsonApiValidationError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; + } +} + +const TestRecordData: typeof V2TestRecordData | typeof V1TestRecordData = V2CACHE_SINGLETON_MANAGER + ? V2TestRecordData + : V1TestRecordData; + const CustomStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - return new TestRecordData(); + return new TestRecordData(storeWrapper, identifier); }, }); @@ -239,6 +342,9 @@ module('integration/record-data - Custom RecordData Implementations', function ( calledUnloadRecord++; } + rollbackAttrs() { + calledRollbackAttributes++; + } rollbackAttributes() { calledRollbackAttributes++; } @@ -255,7 +361,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - return new LifecycleRecordData(); + return new LifecycleRecordData(storeWrapper, identifier); }, }); @@ -362,21 +468,38 @@ module('integration/record-data - Custom RecordData Implementations', function ( return false; } + changedAttrs(): any { + return { name: ['old', 'new'] }; + } + + hasChangedAttrs(): boolean { + return false; + } + + setAttr(identifier: StableRecordIdentifier, key: string, value: any) { + assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); + assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); + } + setDirtyAttribute(key: string, value: any) { assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); } - getAttr(key: string): string { + getAttr(identifier: StableRecordIdentifier, key: string): string { calledGet++; - assert.strictEqual(key, 'name', 'key passed to getAttr'); + if (V2CACHE_SINGLETON_MANAGER) { + assert.strictEqual(key, 'name', 'key passed to getAttr'); + } else { + assert.strictEqual(identifier as unknown as string, 'name', 'key passed to getAttr'); + } return 'new attribute'; } } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - return new AttributeRecordData(); + return new AttributeRecordData(storeWrapper, identifier); }, }); @@ -412,12 +535,12 @@ module('integration/record-data - Custom RecordData Implementations', function ( let belongsToReturnValue = { data: { id: '1', type: 'person' } }; class RelationshipRecordData extends TestRecordData { - constructor(storeWrapper) { - super(); - this._storeWrapper = storeWrapper; + getBelongsTo(key: string) { + assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); + return belongsToReturnValue; } - getBelongsTo(key: string) { + getRelationship(identifier: StableRecordIdentifier, key: string) { assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); return belongsToReturnValue; } @@ -427,12 +550,17 @@ module('integration/record-data - Custom RecordData Implementations', function ( assert.strictEqual(key, 'landlord', 'Passed correct key to setBelongsTo'); assert.strictEqual(recordData.getResourceIdentifier().id, '2', 'Passed correct RD to setBelongsTo'); } + + setBelongsTo(identifier: StableRecordIdentifier, key: string, value: StableRecordIdentifier | null) { + assert.strictEqual(key, 'landlord', 'Passed correct key to setBelongsTo'); + assert.strictEqual(value?.id, '2', 'Passed correct Identifier to setBelongsTo'); + } } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { if (identifier.type === 'house') { - return new RelationshipRecordData(storeWrapper); + return new RelationshipRecordData(storeWrapper, identifier); } else { return this._super(identifier, storeWrapper); } @@ -465,29 +593,42 @@ module('integration/record-data - Custom RecordData Implementations', function ( let belongsToReturnValue = { data: { id: '1', type: 'person' } }; - class RelationshipRecordData extends TestRecordData { - declare identifier: StableRecordIdentifier; - constructor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - super(); - this.identifier = identifier; - this._storeWrapper = storeWrapper; - } + let RelationshipRecordData; + if (V2CACHE_SINGLETON_MANAGER) { + RelationshipRecordData = class extends TestRecordData { + getRelationship(identifier: StableRecordIdentifier, key: string) { + assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); + return belongsToReturnValue; + } - getBelongsTo(key: string) { - assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); - return belongsToReturnValue; - } + setBelongsTo( + this: V2TestRecordData, + identifier: StableRecordIdentifier, + key: string, + value: StableRecordIdentifier | null + ) { + belongsToReturnValue = { data: { id: '3', type: 'person' } }; + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'landlord'); + } + }; + } else { + RelationshipRecordData = class extends TestRecordData { + getBelongsTo(key: string) { + assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); + return belongsToReturnValue; + } - setDirtyBelongsTo(key: string, recordData: this | null) { - belongsToReturnValue = { data: { id: '3', type: 'person' } }; - this._storeWrapper.notifyChange(this.identifier, 'relationships', 'landlord'); - } + setDirtyBelongsTo(this: V1TestRecordData, key: string, recordData: this | null) { + belongsToReturnValue = { data: { id: '3', type: 'person' } }; + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'landlord'); + } + }; } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { if (identifier.type === 'house') { - return new RelationshipRecordData(identifier, storeWrapper); + return new RelationshipRecordData(storeWrapper, identifier); } else { return this._super(identifier, storeWrapper); } @@ -521,52 +662,51 @@ module('integration/record-data - Custom RecordData Implementations', function ( let { owner } = this; - let calledAddToHasMany = 0; - let calledRemoveFromHasMany = 0; let hasManyReturnValue = { data: [{ id: '1', type: 'person' }] }; class RelationshipRecordData extends TestRecordData { - constructor(storeWrapper) { - super(); - this._storeWrapper = storeWrapper; - } - getHasMany(key: string) { return hasManyReturnValue; } - - // TODO: investigate addToHasMany being called during unloading - // Use correct interface once imports have been fix + getRelationship() { + return hasManyReturnValue; + } addToHasMany(key: string, recordDatas: any[], idx?: number) { - // dealing with getting called during unload - if (calledAddToHasMany === 1) { - return; + if (V2CACHE_SINGLETON_MANAGER) { + const key: string = arguments[1]; + const identifiers: StableRecordIdentifier[] = arguments[2]; + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(identifiers[0].id, '2', 'Passed correct RD to addToHasMany'); + } else { + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); } - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); - calledAddToHasMany++; } - removeFromHasMany(key: string, recordDatas: any[]) { - // dealing with getting called during unload - if (calledRemoveFromHasMany === 1) { - return; + if (V2CACHE_SINGLETON_MANAGER) { + const key: string = arguments[1]; + const identifiers: StableRecordIdentifier[] = arguments[2]; + assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); + assert.strictEqual(identifiers[0].id, '1', 'Passed correct RD to removeFromHasMany'); + } else { + assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); + assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '1', 'Passed correct RD to removeFromHasMany'); } - assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '1', 'Passed correct RD to removeFromHasMany'); - calledRemoveFromHasMany++; } - setDirtyHasMany(key: string, recordDatas: any[]) { assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '3', 'Passed correct RD to addToHasMany'); } + setHasMany(identifier: StableRecordIdentifier, key: string, values: StableRecordIdentifier[]) { + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(values[0].id, '3', 'Passed correct RD to addToHasMany'); + } } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { if (identifier.type === 'house') { - return new RelationshipRecordData(storeWrapper); + return new RelationshipRecordData(storeWrapper, identifier); } else { return this._super(identifier, storeWrapper); } @@ -607,31 +747,26 @@ module('integration/record-data - Custom RecordData Implementations', function ( assert.expect(10); let { owner } = this; - let calledAddToHasMany = 0; - let calledRemoveFromHasMany = 0; let hasManyReturnValue = { data: [{ id: '1', type: 'person' }] }; class RelationshipRecordData extends TestRecordData { - declare identifier: StableRecordIdentifier; - constructor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { - super(); - this.identifier = identifier; - this._storeWrapper = storeWrapper; - } - getHasMany(key: string) { return hasManyReturnValue; } + getRelationship() { + return hasManyReturnValue; + } - // TODO: investigate addToHasMany being called during unloading - addToHasMany(key: string, recordDatas: any[], idx?: number) { - // dealing with getting called during unload - if (calledAddToHasMany === 1) { - return; + addToHasMany(this: V1TestRecordData, key: string, recordDatas: any[], idx?: number) { + if (V2CACHE_SINGLETON_MANAGER) { + const key: string = arguments[1]; + const identifiers: StableRecordIdentifier[] = arguments[2]; + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(identifiers[0].id, '2', 'Passed correct RD to addToHasMany'); + } else { + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); } - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); - calledAddToHasMany++; hasManyReturnValue = { data: [ @@ -639,22 +774,24 @@ module('integration/record-data - Custom RecordData Implementations', function ( { id: '2', type: 'person' }, ], }; - this._storeWrapper.notifyChange(this.identifier, 'relationships', 'tenants'); + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); } - removeFromHasMany(key: string, recordDatas: any[]) { - // dealing with getting called during unload - if (calledRemoveFromHasMany === 1) { - return; + removeFromHasMany(this: V1TestRecordData, key: string, recordDatas: any[]) { + if (V2CACHE_SINGLETON_MANAGER) { + const key: string = arguments[1]; + const identifiers: StableRecordIdentifier[] = arguments[2]; + assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); + assert.strictEqual(identifiers[0].id, '2', 'Passed correct RD to removeFromHasMany'); + } else { + assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); + assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to removeFromHasMany'); } - assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to removeFromHasMany'); - calledRemoveFromHasMany++; hasManyReturnValue = { data: [{ id: '1', type: 'person' }] }; - this._storeWrapper.notifyChange(this.identifier, 'relationships', 'tenants'); + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); } - setDirtyHasMany(key: string, recordDatas: any[]) { + setDirtyHasMany(this: V1TestRecordData, key: string, recordDatas: any[]) { assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '3', 'Passed correct RD to addToHasMany'); hasManyReturnValue = { @@ -663,14 +800,31 @@ module('integration/record-data - Custom RecordData Implementations', function ( { id: '2', type: 'person' }, ], }; - this._storeWrapper.notifyChange(this.identifier, 'relationships', 'tenants'); + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); + } + + setHasMany( + this: V2TestRecordData, + identifier: StableRecordIdentifier, + key: string, + values: StableRecordIdentifier[] + ) { + assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(values[0].id, '3', 'Passed correct RD to addToHasMany'); + hasManyReturnValue = { + data: [ + { id: '1', type: 'person' }, + { id: '2', type: 'person' }, + ], + }; + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); } } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { if (identifier.type === 'house') { - return new RelationshipRecordData(identifier, storeWrapper); + return new RelationshipRecordData(storeWrapper, identifier); } else { return this._super(identifier, storeWrapper); } diff --git a/packages/canary-features/addon/default-features.ts b/packages/canary-features/addon/default-features.ts index 94c3fbe0cf0..7d739577185 100644 --- a/packages/canary-features/addon/default-features.ts +++ b/packages/canary-features/addon/default-features.ts @@ -172,5 +172,5 @@ export default { SAMPLE_FEATURE_FLAG: null, V2CACHE_SINGLETON_RECORD_DATA: true, - V2CACHE_SINGLETON_MANAGER: null, + V2CACHE_SINGLETON_MANAGER: true, }; diff --git a/packages/model/addon/-private/many-array.ts b/packages/model/addon/-private/many-array.ts index 7c4a8a88902..ddf390287fb 100644 --- a/packages/model/addon/-private/many-array.ts +++ b/packages/model/addon/-private/many-array.ts @@ -282,7 +282,7 @@ export default class ManyArray extends MutableArrayWithObject 0) { assert( 'The third argument to replace needs to be an array.', Array.isArray(objects) || EmberArray.detect(objects)