From 15934aaf1a813d7d9d6ca446b59dde01a0d4f9e4 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 16 Nov 2023 19:00:35 -0800 Subject: [PATCH 1/5] chore: port tests from 4-6 for emergent behavior --- .../emergent-behavior/recovery-test.ts | 1581 +++++++++++++++++ 1 file changed, 1581 insertions(+) create mode 100644 tests/main/tests/integration/emergent-behavior/recovery-test.ts diff --git a/tests/main/tests/integration/emergent-behavior/recovery-test.ts b/tests/main/tests/integration/emergent-behavior/recovery-test.ts new file mode 100644 index 00000000000..9fb0282a42f --- /dev/null +++ b/tests/main/tests/integration/emergent-behavior/recovery-test.ts @@ -0,0 +1,1581 @@ +import { DEBUG } from '@glimmer/env'; + +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { Snapshot } from '@ember-data/store/-private'; +import type { ModelSchema } from '@ember-data/types/q/ds-model'; + +class User extends Model { + @attr declare name: string; + @hasMany('user', { async: false, inverse: null }) declare friends: User[]; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: User; + @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: User[]; +} + +module('Emergent Behavior - Recovery', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function (assert) { + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store') as Store; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + bestFriend: { + data: { type: 'user', id: '2' }, + }, + }, + }, + }); + }); + + module('belongsTo', function () { + test('When a sync relationship is accessed before load', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // @ts-expect-error Model is not typed in 4.6 + assert.true(bestFriend.isEmpty, 'the relationship is empty'); + // @ts-expect-error Model is not typed in 4.6 + assert.strictEqual(bestFriend.id, '2', 'the relationship id is present'); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + bestFriend: { data: { type: 'user', id: '3' } }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and later mutated', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + user.bestFriend = peter; + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and then later attempted to be found via findRecord', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + await store.findRecord('user', '2'); + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and a later attempt to load via findRecord errors', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // in production because we do not error above the call to getAttr will populate the _attributes object + // in the cache, leading recordData.isEmpty() to return false, thus moving the record into a "loaded" state + // which additionally means that findRecord is treated as a background request. + // + // for this testwe care more that a request is made, than whether it was foreground or background so we force + // the request to be foreground by using reload: true + await store.findRecord('user', '2', { reload: true }).catch(() => { + assert.step('we error'); + }); + assert.verifySteps(['findRecord', 'we error'], 'we called findRecord'); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + + // in production we do not error + assert.ok(true, 'accessing the relationship should not throw'); + + // in DEBUG we should error for this assert + // this is a surprise, because usually failed load attempts result in records being fully removed + // from the store, and so we would expect the relationship to be null + assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); + } catch (e) { + // In DEBUG we should error + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + if (DEBUG) { + assert.strictEqual( + (e as Error).message, + `Cannot read properties of null (reading 'name')`, + 'we get the expected error' + ); + } + } + }); + }); + + module('hasMany', function () { + test('When a sync relationship is accessed before load', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { data: [{ type: 'user', id: '3' }] }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is NOT empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load, records are later loaded, and then it is updated by related record deletion', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + const peter = store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is still INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + this.owner.register( + 'adapter:application', + class { + deleteRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + return Promise.resolve({ + data: null, + }); + } + static create() { + return new this(); + } + } + ); + + store.deleteRecord(peter); + await store.saveRecord(peter); + store.unloadRecord(peter); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship state is now correct'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + }); + + test('When a sync relationship is accessed before load and later updated by remote inverse removal', async function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + store.push({ + data: { + type: 'local-user', + id: '4', + relationships: { + friends: { data: [] }, + }, + }, + }); + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated directly', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + + try { + user.friends.pushObject(peter); + assert.notOk(DEBUG, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via add by inverse', async function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '5', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '5') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // add user2 to user1's friends via inverse + try { + user2.friends.pushObject(user1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via remove by inverse', async function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + try { + user2.friends.removeObject(user1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: specified)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + if (snapshot.include === 'frenemies') { + assert.deepEqual(snapshot._attributes, { name: 'Rey' }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + relationships: { + frenemies: { + data: [{ type: 'user', id: '1' }], + }, + }, + }, + }); + } + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.frenemies; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.frenemies; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.frenemies; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + + // attempt to find the missing record with sideload + try { + await store.findRecord('user', '4', { reload: true, include: 'frenemies' }); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + }); + + test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in DEBUG we error and should not reach here + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In DEBUG we should reach here, in production we should not + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(false, 'finding the missing record should throw'); + } catch (e) { + assert.ok(true, `finding the missing record should throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship is correct'); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + // @ts-expect-error Model is not typed in 4.6 + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + }); + }); +}); From 280f60c7247dd7ec5598742b636866b6e312c91c Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 17 Nov 2023 10:58:47 -0800 Subject: [PATCH 2/5] fixup test file for 4.12 --- .eslintignore | 1 + .gitignore | 1 + .../emergent-behavior/recovery-test.ts | 284 +++++++----------- 3 files changed, 116 insertions(+), 170 deletions(-) diff --git a/.eslintignore b/.eslintignore index e7582bba1aa..42b3e43f80d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ # compiled output **/dist/ +**/dist-*/ **/dist-control/ **/dist-experiment/ **/tmp/ diff --git a/.gitignore b/.gitignore index 1dbb1e69b2d..af5330313bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /packages/-ember-data/docs/ concat-stats-for dist +dist-* tmp packages/tracking/addon packages/request/addon diff --git a/tests/main/tests/integration/emergent-behavior/recovery-test.ts b/tests/main/tests/integration/emergent-behavior/recovery-test.ts index 9fb0282a42f..c2a5f75752b 100644 --- a/tests/main/tests/integration/emergent-behavior/recovery-test.ts +++ b/tests/main/tests/integration/emergent-behavior/recovery-test.ts @@ -1,14 +1,18 @@ -import { DEBUG } from '@glimmer/env'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import { DEBUG } from '@ember-data/env'; +import type { Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import type Store from '@ember-data/store'; -import type { Snapshot } from '@ember-data/store/-private'; import type { ModelSchema } from '@ember-data/types/q/ds-model'; +let IS_DEBUG = false; + +if (DEBUG) { + IS_DEBUG = true; +} class User extends Model { @attr declare name: string; @hasMany('user', { async: false, inverse: null }) declare friends: User[]; @@ -47,7 +51,7 @@ module('Emergent Behavior - Recovery', function (hooks) { }); module('belongsTo', function () { - test('When a sync relationship is accessed before load', async function (assert) { + test('When a sync relationship is accessed before load', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -57,23 +61,21 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const bestFriend = user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); - // @ts-expect-error Model is not typed in 4.6 + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.true(bestFriend.isEmpty, 'the relationship is empty'); - // @ts-expect-error Model is not typed in 4.6 assert.strictEqual(bestFriend.id, '2', 'the relationship id is present'); assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); } }); - test('When a sync relationship is accessed before load and later updated remotely', async function (assert) { + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -83,11 +85,11 @@ module('Emergent Behavior - Recovery', function (hooks) { try { user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } store.push({ @@ -115,7 +117,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); }); - test('When a sync relationship is accessed before load and later mutated', async function (assert) { + test('When a sync relationship is accessed before load and later mutated', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -125,11 +127,11 @@ module('Emergent Behavior - Recovery', function (hooks) { try { user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; @@ -141,7 +143,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); }); - test('When a sync relationship is accessed before load and then later sideloaded', async function (assert) { + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -149,12 +151,12 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const bestFriend = user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } // sideload the relationship @@ -174,7 +176,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); } catch (e) { - // In DEBUG we should reach here, in production we should not + // In IS_DEBUG we should reach here, in production we should not assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); } }); @@ -208,12 +210,12 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const bestFriend = user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } // sideload the relationship @@ -226,7 +228,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); } catch (e) { - // In DEBUG we should reach here, in production we should not + // In IS_DEBUG we should reach here, in production we should not assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); } }); @@ -253,12 +255,12 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const bestFriend = user.bestFriend; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } // in production because we do not error above the call to getAttr will populate the _attributes object @@ -279,14 +281,14 @@ module('Emergent Behavior - Recovery', function (hooks) { // in production we do not error assert.ok(true, 'accessing the relationship should not throw'); - // in DEBUG we should error for this assert + // in IS_DEBUG we should error for this assert // this is a surprise, because usually failed load attempts result in records being fully removed // from the store, and so we would expect the relationship to be null assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); } catch (e) { - // In DEBUG we should error - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - if (DEBUG) { + // In IS_DEBUG we should error + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + if (IS_DEBUG) { assert.strictEqual( (e as Error).message, `Cannot read properties of null (reading 'name')`, @@ -298,7 +300,7 @@ module('Emergent Behavior - Recovery', function (hooks) { }); module('hasMany', function () { - test('When a sync relationship is accessed before load', async function (assert) { + test('When a sync relationship is accessed before load', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -308,11 +310,10 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -320,12 +321,11 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -333,7 +333,7 @@ module('Emergent Behavior - Recovery', function (hooks) { } }); - test('When a sync relationship is accessed before load and later updated remotely', async function (assert) { + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -343,20 +343,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -390,7 +388,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 1, 'the relationship is NOT empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 1, 'the relationship reference contains the expected ids' @@ -398,7 +395,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 1, 'the relationship reference contains the expected ids' @@ -417,20 +413,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -471,7 +465,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is still INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -479,7 +472,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -512,7 +504,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 2, 'the relationship state is now correct'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' @@ -520,7 +511,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' @@ -529,7 +519,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); }); - test('When a sync relationship is accessed before load and later updated by remote inverse removal', async function (assert) { + test('When a sync relationship is accessed before load and later updated by remote inverse removal', function (assert) { class LocalUser extends Model { @attr declare name: string; @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; @@ -577,20 +567,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -614,18 +602,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' @@ -634,7 +620,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); }); - test('When a sync relationship is accessed before load and later mutated directly', async function (assert) { + test('When a sync relationship is accessed before load and later mutated directly', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -644,20 +630,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -669,31 +653,29 @@ module('Emergent Behavior - Recovery', function (hooks) { try { user.friends.pushObject(peter); - assert.notOk(DEBUG, 'mutating the relationship should not throw'); + assert.notOk(IS_DEBUG, 'mutating the relationship should not throw'); } catch (e) { - assert.ok(DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); } // access the relationship again try { const friends = user.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual( friends.length, 1, 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' ); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 4, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -702,7 +684,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); }); - test('When a sync relationship is accessed before load and later mutated via add by inverse', async function (assert) { + test('When a sync relationship is accessed before load and later mutated via add by inverse', function (assert) { class LocalUser extends Model { @attr declare name: string; @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; @@ -750,20 +732,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -784,22 +764,20 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual( friends.length, 1, 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' ); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 4, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 4, 'the relationship reference contains the expected ids' @@ -808,7 +786,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); }); - test('When a sync relationship is accessed before load and later mutated via remove by inverse', async function (assert) { + test('When a sync relationship is accessed before load and later mutated via remove by inverse', function (assert) { class LocalUser extends Model { @attr declare name: string; @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; @@ -856,22 +834,20 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -892,20 +868,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user1.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user1.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' @@ -914,7 +888,7 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); }); - test('When a sync relationship is accessed before load and then later sideloaded', async function (assert) { + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { const store = this.owner.lookup('service:store') as Store; const user = store.peekRecord('user', '1') as unknown as User; @@ -922,20 +896,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -979,7 +951,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -987,7 +958,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1019,7 +989,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1027,7 +996,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1064,20 +1032,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1109,18 +1075,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1149,18 +1113,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1183,7 +1145,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1191,7 +1152,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1265,20 +1225,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.frenemies; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1310,18 +1268,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.frenemies; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1350,18 +1306,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.frenemies; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1384,7 +1338,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1392,7 +1345,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1443,20 +1395,18 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - // in DEBUG we error and should not reach here - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - // In DEBUG we should reach here, in production we should not - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1488,18 +1438,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1528,18 +1476,16 @@ module('Emergent Behavior - Recovery', function (hooks) { try { const friends = user.friends; - assert.notOk(DEBUG, 'accessing the relationship should not throw'); + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' ); } catch (e) { - assert.ok(DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 3, 'the relationship reference contains the expected ids' @@ -1562,7 +1508,6 @@ module('Emergent Behavior - Recovery', function (hooks) { assert.ok(true, 'accessing the relationship should not throw'); assert.strictEqual(friends.length, 2, 'the relationship is correct'); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' @@ -1570,7 +1515,6 @@ module('Emergent Behavior - Recovery', function (hooks) { } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); assert.strictEqual( - // @ts-expect-error Model is not typed in 4.6 user.hasMany('friends').ids().length, 2, 'the relationship reference contains the expected ids' From 11bdf62417acf97c488a79423108f161e2b90b5d Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 17 Nov 2023 11:06:57 -0800 Subject: [PATCH 3/5] restructure test files --- .../emergent-behavior/recovery-test.ts | 1525 ----------------- .../recovery/belongs-to-test.ts | 290 ++++ .../recovery/has-many-test.ts | 1270 ++++++++++++++ 3 files changed, 1560 insertions(+), 1525 deletions(-) delete mode 100644 tests/main/tests/integration/emergent-behavior/recovery-test.ts create mode 100644 tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts create mode 100644 tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts diff --git a/tests/main/tests/integration/emergent-behavior/recovery-test.ts b/tests/main/tests/integration/emergent-behavior/recovery-test.ts deleted file mode 100644 index c2a5f75752b..00000000000 --- a/tests/main/tests/integration/emergent-behavior/recovery-test.ts +++ /dev/null @@ -1,1525 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import { DEBUG } from '@ember-data/env'; -import type { Snapshot } from '@ember-data/legacy-compat/-private'; -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import type Store from '@ember-data/store'; -import type { ModelSchema } from '@ember-data/types/q/ds-model'; - -let IS_DEBUG = false; - -if (DEBUG) { - IS_DEBUG = true; -} -class User extends Model { - @attr declare name: string; - @hasMany('user', { async: false, inverse: null }) declare friends: User[]; - @belongsTo('user', { async: false, inverse: null }) declare bestFriend: User; - @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: User[]; -} - -module('Emergent Behavior - Recovery', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function (assert) { - this.owner.register('model:user', User); - - const store = this.owner.lookup('service:store') as Store; - store.push({ - data: { - type: 'user', - id: '1', - attributes: { - name: 'Chris Wagenet', - }, - relationships: { - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - bestFriend: { - data: { type: 'user', id: '2' }, - }, - }, - }, - }); - }); - - module('belongsTo', function () { - test('When a sync relationship is accessed before load', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - const bestFriend = user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.true(bestFriend.isEmpty, 'the relationship is empty'); - assert.strictEqual(bestFriend.id, '2', 'the relationship id is present'); - assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - } - }); - - test('When a sync relationship is accessed before load and later updated remotely', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - bestFriend: { data: { type: 'user', id: '3' } }, - }, - }, - included: [ - { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - ], - }); - - // access the relationship again - const bestFriend = user.bestFriend; - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); - }); - - test('When a sync relationship is accessed before load and later mutated', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - - const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; - user.bestFriend = peter; - - // access the relationship again - const bestFriend = user.bestFriend; - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); - }); - - test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - // access the relationship before load - try { - const bestFriend = user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - - // sideload the relationship - store.push({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - - // access the relationship after sideload - try { - const bestFriend = user.bestFriend; - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - }); - - test('When a sync relationship is accessed before load and then later attempted to be found via findRecord', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - this.owner.register( - 'adapter:application', - class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - assert.step('findRecord'); - assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); - return Promise.resolve({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - } - static create() { - return new this(); - } - } - ); - - // access the relationship before load - try { - const bestFriend = user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - - // sideload the relationship - await store.findRecord('user', '2'); - assert.verifySteps(['findRecord'], 'we called findRecord'); - - // access the relationship after sideload - try { - const bestFriend = user.bestFriend; - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - }); - - test('When a sync relationship is accessed before load and a later attempt to load via findRecord errors', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - this.owner.register( - 'adapter:application', - class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - assert.step('findRecord'); - assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); - - return Promise.reject(new Error('404 - Not Found')); - } - static create() { - return new this(); - } - } - ); - - // access the relationship before load - try { - const bestFriend = user.bestFriend; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - - // in production because we do not error above the call to getAttr will populate the _attributes object - // in the cache, leading recordData.isEmpty() to return false, thus moving the record into a "loaded" state - // which additionally means that findRecord is treated as a background request. - // - // for this testwe care more that a request is made, than whether it was foreground or background so we force - // the request to be foreground by using reload: true - await store.findRecord('user', '2', { reload: true }).catch(() => { - assert.step('we error'); - }); - assert.verifySteps(['findRecord', 'we error'], 'we called findRecord'); - - // access the relationship after sideload - try { - const bestFriend = user.bestFriend; - - // in production we do not error - assert.ok(true, 'accessing the relationship should not throw'); - - // in IS_DEBUG we should error for this assert - // this is a surprise, because usually failed load attempts result in records being fully removed - // from the store, and so we would expect the relationship to be null - assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); - } catch (e) { - // In IS_DEBUG we should error - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - if (IS_DEBUG) { - assert.strictEqual( - (e as Error).message, - `Cannot read properties of null (reading 'name')`, - 'we get the expected error' - ); - } - } - }); - }); - - module('hasMany', function () { - test('When a sync relationship is accessed before load', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - }); - - test('When a sync relationship is accessed before load and later updated remotely', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - friends: { data: [{ type: 'user', id: '3' }] }, - }, - }, - included: [ - { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - ], - }); - - // access the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 1, 'the relationship is NOT empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 1, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 1, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); - }); - - test('When a sync relationship is accessed before load, records are later loaded, and then it is updated by related record deletion', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - const peter = store.push({ - data: { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - included: [ - { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - { - type: 'user', - id: '4', - attributes: { - name: 'Rey', - }, - }, - ], - }); - - // access the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is still INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); - - this.owner.register( - 'adapter:application', - class { - deleteRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - return Promise.resolve({ - data: null, - }); - } - static create() { - return new this(); - } - } - ); - - store.deleteRecord(peter); - await store.saveRecord(peter); - store.unloadRecord(peter); - - // access the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 2, 'the relationship state is now correct'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); - }); - - test('When a sync relationship is accessed before load and later updated by remote inverse removal', function (assert) { - class LocalUser extends Model { - @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; - } - this.owner.register('model:local-user', LocalUser); - const store = this.owner.lookup('service:store') as Store; - const user1 = store.push({ - data: { - type: 'local-user', - id: '1', - attributes: { - name: 'Chris Wagenet', - }, - relationships: { - friends: { - data: [ - { type: 'local-user', id: '2' }, - { type: 'local-user', id: '3' }, - { type: 'local-user', id: '4' }, - ], - }, - }, - }, - included: [ - { - type: 'local-user', - id: '4', - attributes: { - name: 'Krystan', - }, - relationships: { - friends: { - data: [{ type: 'local-user', id: '1' }], - }, - }, - }, - ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; - - assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); - assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); - - // access the relationship before load - try { - const friends = user1.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - - // remove user2 from user1's friends via inverse - store.push({ - data: { - type: 'local-user', - id: '4', - relationships: { - friends: { data: [] }, - }, - }, - }); - - // access the relationship again - try { - const friends = user1.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - }); - - test('When a sync relationship is accessed before load and later mutated directly', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; - - try { - user.friends.pushObject(peter); - assert.notOk(IS_DEBUG, 'mutating the relationship should not throw'); - } catch (e) { - assert.ok(IS_DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); - } - - // access the relationship again - try { - const friends = user.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual( - friends.length, - 1, - 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' - ); - assert.strictEqual( - user.hasMany('friends').ids().length, - 4, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); - }); - - test('When a sync relationship is accessed before load and later mutated via add by inverse', function (assert) { - class LocalUser extends Model { - @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; - } - this.owner.register('model:local-user', LocalUser); - const store = this.owner.lookup('service:store') as Store; - const user1 = store.push({ - data: { - type: 'local-user', - id: '1', - attributes: { - name: 'Chris Wagenet', - }, - relationships: { - friends: { - data: [ - { type: 'local-user', id: '2' }, - { type: 'local-user', id: '3' }, - { type: 'local-user', id: '4' }, - ], - }, - }, - }, - included: [ - { - type: 'local-user', - id: '5', - attributes: { - name: 'Krystan', - }, - relationships: { - friends: { - data: [], - }, - }, - }, - ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '5') as unknown as LocalUser; - - assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); - assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); - - // access the relationship before load - try { - const friends = user1.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - - // add user2 to user1's friends via inverse - try { - user2.friends.pushObject(user1); - assert.ok(true, 'mutating the relationship should not throw'); - } catch (e) { - assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); - } - - // access the relationship again - try { - const friends = user1.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual( - friends.length, - 1, - 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' - ); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 4, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user1.hasMany('friends').ids().length, - 4, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - }); - - test('When a sync relationship is accessed before load and later mutated via remove by inverse', function (assert) { - class LocalUser extends Model { - @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; - } - this.owner.register('model:local-user', LocalUser); - const store = this.owner.lookup('service:store') as Store; - const user1 = store.push({ - data: { - type: 'local-user', - id: '1', - attributes: { - name: 'Chris Wagenet', - }, - relationships: { - friends: { - data: [ - { type: 'local-user', id: '2' }, - { type: 'local-user', id: '3' }, - { type: 'local-user', id: '4' }, - ], - }, - }, - }, - included: [ - { - type: 'local-user', - id: '4', - attributes: { - name: 'Krystan', - }, - relationships: { - friends: { - data: [{ type: 'local-user', id: '1' }], - }, - }, - }, - ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; - - assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); - assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); - - // access the relationship before load - try { - const friends = user1.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); - - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - - assert.strictEqual( - user1.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - - // remove user2 from user1's friends via inverse - try { - user2.friends.removeObject(user1); - assert.ok(true, 'mutating the relationship should not throw'); - } catch (e) { - assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); - } - - // access the relationship again - try { - const friends = user1.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); - - assert.strictEqual( - user1.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - - assert.strictEqual( - user1.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); - }); - - test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - // sideload the relationships - store.push({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - store.push({ - data: { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - }); - store.push({ - data: { - type: 'user', - id: '4', - attributes: { - name: 'Rey', - }, - }, - }); - - // access the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); - - // attempt notify of the relationship - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - }, - }, - }); - - // access the relationship - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - }); - - test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: null)', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - this.owner.register( - 'adapter:application', - class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - assert.step('findRecord'); - assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); - return Promise.resolve({ - data: { - type: 'user', - id: '4', - attributes: { - name: 'Rey', - }, - }, - }); - } - static create() { - return new this(); - } - } - ); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - // sideload two of the relationships - store.push({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - store.push({ - data: { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - }); - - // access the relationship again - try { - const friends = user.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); - - // attempt notify of the relationship - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - }, - }, - }); - - // access the relationship - try { - const friends = user.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - // attempt to find the missing record - try { - await store.findRecord('user', '4'); - assert.ok(true, 'finding the missing record should not throw'); - } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); - } - assert.verifySteps(['findRecord'], 'we called findRecord'); - - // check the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - }); - - test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: specified)', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - store.push({ - data: { - type: 'user', - id: '1', - attributes: { - name: 'Chris Wagenet', - }, - relationships: { - frenemies: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - }, - }, - }); - this.owner.register( - 'adapter:application', - class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - assert.step('findRecord'); - if (snapshot.include === 'frenemies') { - assert.deepEqual(snapshot._attributes, { name: 'Rey' }, 'the snapshot has the correct attributes'); - - return Promise.resolve({ - data: { - type: 'user', - id: '4', - attributes: { - name: 'Rey', - }, - relationships: { - frenemies: { - data: [{ type: 'user', id: '1' }], - }, - }, - }, - }); - } - assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); - - return Promise.resolve({ - data: { - type: 'user', - id: '4', - attributes: { - name: 'Rey', - }, - }, - }); - } - static create() { - return new this(); - } - } - ); - - // access the relationship before load - try { - const friends = user.frenemies; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - // sideload two of the relationships - store.push({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - store.push({ - data: { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - }); - - // access the relationship again - try { - const friends = user.frenemies; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); - - // attempt notify of the relationship - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - frenemies: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - }, - }, - }); - - // access the relationship - try { - const friends = user.frenemies; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - // attempt to find the missing record - try { - await store.findRecord('user', '4'); - assert.ok(true, 'finding the missing record should not throw'); - } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); - } - assert.verifySteps(['findRecord'], 'we called findRecord'); - - // check the relationship again - try { - const friends = user.frenemies; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); - - // attempt to find the missing record with sideload - try { - await store.findRecord('user', '4', { reload: true, include: 'frenemies' }); - assert.ok(true, 'finding the missing record should not throw'); - } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); - } - assert.verifySteps(['findRecord'], 'we called findRecord'); - - // check the relationship again - try { - const friends = user.frenemies; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); - }); - - test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; - this.owner.register( - 'adapter:application', - class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { - assert.step('findRecord'); - assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); - - return Promise.reject(new Error('404 - Not Found')); - } - static create() { - return new this(); - } - } - ); - - // access the relationship before load - try { - const friends = user.friends; - - // in IS_DEBUG we error and should not reach here - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - // In IS_DEBUG we should reach here, in production we should not - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - - // sideload two of the relationships - store.push({ - data: { - type: 'user', - id: '2', - attributes: { - name: 'Krystan', - }, - }, - }); - store.push({ - data: { - type: 'user', - id: '3', - attributes: { - name: 'Peter', - }, - }, - }); - - // access the relationship again - try { - const friends = user.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); - - // attempt notify of the relationship - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - { type: 'user', id: '4' }, - ], - }, - }, - }, - }); - - // access the relationship - try { - const friends = user.friends; - - assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' - ); - } - - // attempt to find the missing record - try { - await store.findRecord('user', '4'); - assert.ok(false, 'finding the missing record should throw'); - } catch (e) { - assert.ok(true, `finding the missing record should throw, received ${(e as Error).message}`); - } - assert.verifySteps(['findRecord'], 'we called findRecord'); - - // check the relationship again - try { - const friends = user.friends; - - assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 2, 'the relationship is correct'); - assert.strictEqual( - user.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } catch (e) { - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); - assert.strictEqual( - user.hasMany('friends').ids().length, - 2, - 'the relationship reference contains the expected ids' - ); - } - }); - }); -}); diff --git a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts new file mode 100644 index 00000000000..a125e82e8bb --- /dev/null +++ b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts @@ -0,0 +1,290 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import { DEBUG } from '@ember-data/env'; +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import Model, { attr, belongsTo } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/types/q/ds-model'; + +let IS_DEBUG = false; + +if (DEBUG) { + IS_DEBUG = true; +} +class User extends Model { + @attr declare name: string; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: User; +} + +module('Emergent Behavior > Recovery | belongsTo', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store') as Store; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + }, + }, + }); + }); + + test('When a sync relationship is accessed before load', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.true(bestFriend.isEmpty, 'the relationship is empty'); + assert.strictEqual(bestFriend.id, '2', 'the relationship id is present'); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + bestFriend: { data: { type: 'user', id: '3' } }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and later mutated', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + user.bestFriend = peter; + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and then later attempted to be found via findRecord', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + await store.findRecord('user', '2'); + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and a later attempt to load via findRecord errors', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // in production because we do not error above the call to getAttr will populate the _attributes object + // in the cache, leading recordData.isEmpty() to return false, thus moving the record into a "loaded" state + // which additionally means that findRecord is treated as a background request. + // + // for this testwe care more that a request is made, than whether it was foreground or background so we force + // the request to be foreground by using reload: true + await store.findRecord('user', '2', { reload: true }).catch(() => { + assert.step('we error'); + }); + assert.verifySteps(['findRecord', 'we error'], 'we called findRecord'); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + + // in production we do not error + assert.ok(true, 'accessing the relationship should not throw'); + + // in IS_DEBUG we should error for this assert + // this is a surprise, because usually failed load attempts result in records being fully removed + // from the store, and so we would expect the relationship to be null + assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); + } catch (e) { + // In IS_DEBUG we should error + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + if (IS_DEBUG) { + assert.strictEqual( + (e as Error).message, + `Cannot read properties of null (reading 'name')`, + 'we get the expected error' + ); + } + } + }); +}); diff --git a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts new file mode 100644 index 00000000000..f64b952f15f --- /dev/null +++ b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts @@ -0,0 +1,1270 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import { DEBUG } from '@ember-data/env'; +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import Model, { attr, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/types/q/ds-model'; + +let IS_DEBUG = false; + +if (DEBUG) { + IS_DEBUG = true; +} +class User extends Model { + @attr declare name: string; + @hasMany('user', { async: false, inverse: null }) declare friends: User[]; + @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: User[]; +} + +module('Emergent Behavior > Recovery | hasMany', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store') as Store; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + }); + + test('When a sync relationship is accessed before load', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { data: [{ type: 'user', id: '3' }] }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is NOT empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load, records are later loaded, and then it is updated by related record deletion', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + const peter = store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is still INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + this.owner.register( + 'adapter:application', + class { + deleteRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + return Promise.resolve({ + data: null, + }); + } + static create() { + return new this(); + } + } + ); + + store.deleteRecord(peter); + await store.saveRecord(peter); + store.unloadRecord(peter); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship state is now correct'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + }); + + test('When a sync relationship is accessed before load and later updated by remote inverse removal', function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + store.push({ + data: { + type: 'local-user', + id: '4', + relationships: { + friends: { data: [] }, + }, + }, + }); + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated directly', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + + try { + user.friends.pushObject(peter); + assert.notOk(IS_DEBUG, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(IS_DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via add by inverse', function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '5', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '5') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // add user2 to user1's friends via inverse + try { + user2.friends.pushObject(user1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via remove by inverse', function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }) as unknown as LocalUser; + const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + try { + user2.friends.removeObject(user1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: specified)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + if (snapshot.include === 'frenemies') { + assert.deepEqual(snapshot._attributes, { name: 'Rey' }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + relationships: { + frenemies: { + data: [{ type: 'user', id: '1' }], + }, + }, + }, + }); + } + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.frenemies; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.frenemies; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.frenemies; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + + // attempt to find the missing record with sideload + try { + await store.findRecord('user', '4', { reload: true, include: 'frenemies' }); + assert.ok(true, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + }); + + test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as unknown as User; + this.owner.register( + 'adapter:application', + class { + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(false, 'finding the missing record should throw'); + } catch (e) { + assert.ok(true, `finding the missing record should throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship is correct'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + }); +}); From a3e227ae99c4f921b09f251e73a98eeb6b2673dd Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 17 Nov 2023 14:36:11 -0800 Subject: [PATCH 4/5] fixes and test improvements --- packages/json-api/src/-private/cache.ts | 25 +++++- .../src/-private/caches/instance-cache.ts | 14 +++- .../recovery/belongs-to-test.ts | 32 +++++--- .../recovery/has-many-test.ts | 81 ++++++++++++++----- .../integration/references/belongs-to-test.js | 12 +-- 5 files changed, 126 insertions(+), 38 deletions(-) diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index a804b941864..a02d4d953af 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -927,6 +927,13 @@ export default class JSONAPICache implements Cache { */ getAttr(identifier: StableRecordIdentifier, attr: string): unknown { const cached = this.__peek(identifier, true); + assert(`Cannot retrieve attributes for identifier ${identifier} as it is not present in the cache`, cached); + + // in Prod we try to recover when accessing something that + // doesn't exist + if (!cached) { + return undefined; + } if (cached.localAttrs && attr in cached.localAttrs) { return cached.localAttrs[attr]; } else if (cached.inflightAttrs && attr in cached.inflightAttrs) { @@ -981,8 +988,17 @@ export default class JSONAPICache implements Cache { * @returns { : [, ] } */ changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + const cached = this.__peek(identifier, false); + assert(`Cannot retrieve changed attributes for identifier ${identifier} as it is not present in the cache`, cached); + + // in Prod we try to recover when accessing something that + // doesn't exist + if (!cached) { + return Object.create(null); + } + // TODO freeze in dev - return this.__peek(identifier, false).changes || Object.create(null); + return cached.changes || Object.create(null); } /** @@ -995,6 +1011,13 @@ export default class JSONAPICache implements Cache { */ hasChangedAttrs(identifier: StableRecordIdentifier): boolean { const cached = this.__peek(identifier, true); + assert(`Cannot retrieve changed attributes for identifier ${identifier} as it is not present in the cache`, cached); + + // in Prod we try to recover when accessing something that + // doesn't exist + if (!cached) { + return false; + } return ( (cached.inflightAttrs !== null && Object.keys(cached.inflightAttrs).length > 0) || diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index f25100fc630..7699c85880d 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -264,7 +264,12 @@ export class InstanceCache { } ); } - assert(`Cannot create a record for ${identifier.type + identifier.id} (${identifier.lid}) as no resource data exists`, cache.peek(identifier)); + assert( + `Cannot create a record for ${identifier.type + ':' + String(identifier.id)} (${ + identifier.lid + }) as no resource data exists`, + cache.peek(identifier) + ); record = this.store.instantiateRecord( identifier, properties || {}, @@ -273,7 +278,12 @@ export class InstanceCache { this.store.notifications ); } else { - assert(`Cannot create a record for ${identifier.type + identifier.id} (${identifier.lid}) as no resource data exists`, cache.peek(identifier)); + assert( + `Cannot create a record for ${identifier.type + ':' + String(identifier.id)} (${ + identifier.lid + }) as no resource data exists`, + cache.peek(identifier) + ); record = this.store.instantiateRecord(identifier, properties || {}); } diff --git a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts index a125e82e8bb..41112c4f72e 100644 --- a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts +++ b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts @@ -209,7 +209,13 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { } // sideload the relationship - await store.findRecord('user', '2'); + try { + await store.findRecord('user', '2'); + assert.notOk(IS_DEBUG, `In production, finding the record should succeed`); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `finding the record should not throw, received ${(e as Error).message}`); + } assert.verifySteps(['findRecord'], 'we called findRecord'); // access the relationship after sideload @@ -219,7 +225,7 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); } catch (e) { // In IS_DEBUG we should reach here, in production we should not - assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } }); @@ -266,25 +272,27 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { // access the relationship after sideload try { - const bestFriend = user.bestFriend; + user.bestFriend; // in production we do not error assert.ok(true, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should error + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + try { + const bestFriend = user.bestFriend; // in IS_DEBUG we should error for this assert // this is a surprise, because usually failed load attempts result in records being fully removed // from the store, and so we would expect the relationship to be null assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); } catch (e) { - // In IS_DEBUG we should error - assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); - if (IS_DEBUG) { - assert.strictEqual( - (e as Error).message, - `Cannot read properties of null (reading 'name')`, - 'we get the expected error' - ); - } + assert.strictEqual( + (e as Error).message, + `Cannot read properties of null (reading 'name')`, + 'we get the expected error' + ); } }); }); diff --git a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts index f64b952f15f..e34df58505b 100644 --- a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts +++ b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts @@ -398,7 +398,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; try { - user.friends.pushObject(peter); + user.friends.push(peter); assert.notOk(IS_DEBUG, 'mutating the relationship should not throw'); } catch (e) { assert.ok(IS_DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); @@ -500,7 +500,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { // add user2 to user1's friends via inverse try { - user2.friends.pushObject(user1); + user2.friends.push(user1); assert.ok(true, 'mutating the relationship should not throw'); } catch (e) { assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); @@ -604,7 +604,8 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { // remove user2 from user1's friends via inverse try { - user2.friends.removeObject(user1); + const index = user2.friends.indexOf(user1); + user2.friends.splice(index, 1); assert.ok(true, 'mutating the relationship should not throw'); } catch (e) { assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); @@ -878,9 +879,9 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { // attempt to find the missing record try { await store.findRecord('user', '4'); - assert.ok(true, 'finding the missing record should not throw'); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); } assert.verifySteps(['findRecord'], 'we called findRecord'); @@ -889,11 +890,24 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { const friends = user.friends; assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); assert.strictEqual( user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if debug since we error)' ); } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); @@ -902,6 +916,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { 3, 'the relationship reference contains the expected ids' ); + assert.strictEqual(store.peekAll('user').length, 4, 'the store correctly shows 4 records'); } }); @@ -1071,9 +1086,9 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { // attempt to find the missing record try { await store.findRecord('user', '4'); - assert.ok(true, 'finding the missing record should not throw'); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); } assert.verifySteps(['findRecord'], 'we called findRecord'); @@ -1082,11 +1097,23 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { const friends = user.frenemies; assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); assert.strictEqual( user.hasMany('friends').ids().length, - 3, - 'the relationship reference contains the expected ids' + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if we are a debug build since we error)' ); } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); @@ -1095,15 +1122,15 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { 3, 'the relationship reference contains the expected ids' ); + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); } - assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); // attempt to find the missing record with sideload try { await store.findRecord('user', '4', { reload: true, include: 'frenemies' }); - assert.ok(true, 'finding the missing record should not throw'); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); } catch (e) { - assert.ok(false, `finding the missing record should not throw, received ${(e as Error).message}`); + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); } assert.verifySteps(['findRecord'], 'we called findRecord'); @@ -1112,11 +1139,29 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { const friends = user.frenemies; assert.ok(true, 'accessing the relationship should not throw'); - assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if we are a debug build since we error)' + ); } catch (e) { assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); } - assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); }); test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { diff --git a/tests/main/tests/integration/references/belongs-to-test.js b/tests/main/tests/integration/references/belongs-to-test.js index d403d1eabd9..7af30997cfe 100644 --- a/tests/main/tests/integration/references/belongs-to-test.js +++ b/tests/main/tests/integration/references/belongs-to-test.js @@ -575,18 +575,20 @@ module('integration/references/belongs-to', function (hooks) { this.owner.register('model:familia', Familia); this.owner.register('model:persona', Persona); this.owner.register('adapter:application', class extends JSONAPIAdapter {}); - this.owner.register('serializer:application', class extends JSONAPISerializer { - normalizeResponse(_store, _schema, payload) { - return payload; + this.owner.register( + 'serializer:application', + class extends JSONAPISerializer { + normalizeResponse(_store, _schema, payload) { + return payload; + } } - }); + ); const store = this.owner.lookup('service:store'); const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; adapter.findRecord = function (store, type, id, snapshot) { - debugger; assert.step('findRecord'); assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); From a906ab538d80edea049aaba442974dac62e513ad Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 17 Nov 2023 15:14:43 -0800 Subject: [PATCH 5/5] various fixes --- packages/json-api/src/-private/cache.ts | 4 ++-- packages/model/src/-private/legacy-relationships-support.ts | 5 ++--- packages/store/src/-private/caches/instance-cache.ts | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index a02d4d953af..067e5235a5a 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -358,8 +358,8 @@ export default class JSONAPICache implements Cache { const rels = graph.identifiers.get(identifier); if (rels) { Object.keys(rels).forEach((key) => { - const rel = rels[key]!; - if (rel.definition.isImplicit) { + const rel = rels[key]; + if (!rel || rel.definition.isImplicit) { return; } relationships[key] = (rel as ManyRelationship | BelongsToRelationship).getData(); diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index 04565ffce1a..c0314a4e66f 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -166,14 +166,13 @@ export class LegacySupport { if (relatedIdentifier === null) { return null; } else { - let toReturn = store._instanceCache.getRecord(relatedIdentifier); assert( `You looked up the '${key}' relationship on a '${identifier.type}' with id ${ identifier.id || 'null' } but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (\`belongsTo(, { async: true, inverse: })\`)`, - toReturn === null || store._instanceCache.recordIsLoaded(relatedIdentifier, true) + store._instanceCache.recordIsLoaded(relatedIdentifier, true) ); - return toReturn; + return store._instanceCache.getRecord(relatedIdentifier); } } } diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 7699c85880d..22e3b473841 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -268,7 +268,8 @@ export class InstanceCache { `Cannot create a record for ${identifier.type + ':' + String(identifier.id)} (${ identifier.lid }) as no resource data exists`, - cache.peek(identifier) + // @ts-expect-error managedVersion is private and debug only + Boolean(cache.managedVersion === '1' || cache.peek(identifier)) ); record = this.store.instantiateRecord( identifier,