Skip to content

Commit

Permalink
feat: autotracking for reference id access (#7796)
Browse files Browse the repository at this point in the history
* feat: autotracking for reference id access

* ensure references are torn down

* fix build

* add dep

* add to deps

* fix invalid json:api support and add valid json:api support

* autotracking tests and cleanup

* fix test failure, add comment

* skip tests when feature not available
  • Loading branch information
runspired authored Dec 15, 2021
1 parent 61c61be commit 856f7b3
Show file tree
Hide file tree
Showing 15 changed files with 566 additions and 156 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ module.exports = {
'packages/store/addon/-private/system/snapshot-record-array.ts',
'packages/store/addon/-private/system/schema-definition-service.ts',
'packages/store/addon/-private/system/request-cache.ts',
'packages/store/addon/-private/system/references/belongs-to.ts',
'packages/store/addon/-private/system/references/has-many.ts',
'packages/store/addon/-private/system/references/reference.ts',
'packages/store/addon/-private/system/references/record.ts',
'packages/store/addon/-private/system/record-notification-manager.ts',
Expand Down
154 changes: 154 additions & 0 deletions packages/-ember-data/tests/integration/references/autotracking-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import EmberObject from '@ember/object';
import { getRootElement, render } from '@ember/test-helpers';
import settled from '@ember/test-helpers/settled';

import hbs from 'htmlbars-inline-precompile';
import { module, test } from 'qunit';

import { setupRenderingTest } from 'ember-qunit';

import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { recordIdentifierFor } from '@ember-data/store';

if (CUSTOM_MODEL_CLASS) {
module('integration/references/autotracking', function (hooks) {
setupRenderingTest(hooks);

class User extends Model {
@attr name;
@belongsTo('user', { inverse: null, async: false })
bestFriend;
@hasMany('user', { inverse: null, async: false })
friends;
}

let store, user;
hooks.beforeEach(function () {
const { owner } = this;
owner.register('model:user', User);
store = owner.lookup('service:store');

owner.register(
'adapter:user',
class extends EmberObject {
createRecord() {
return { data: { id: '6', type: 'user' } };
}
}
);
owner.register(
'serializer:user',
class extends EmberObject {
normalizeResponse(_, __, data) {
return data;
}
}
);

user = store.push({
data: {
type: 'user',
id: '1',
attributes: {
name: 'Chris',
},
relationships: {
bestFriend: {
data: { type: 'user', id: '2' },
},
friends: {
data: [{ type: 'user', id: '2' }],
},
},
},
included: [
{ type: 'user', id: '2', attributes: { name: 'Igor' } },
{ type: 'user', id: '3', attributes: { name: 'David' } },
{ type: 'user', id: '4', attributes: { name: 'Scott' } },
{ type: 'user', id: '5', attributes: { name: 'Rob' } },
],
});
});

test('BelongsToReference.id() is autotracked', async function (assert) {
class TestContext {
user = user;

get bestFriendId() {
return this.user.belongsTo('bestFriend').id();
}
}

const testContext = new TestContext();
this.set('context', testContext);
await render(hbs`id: {{if this.context.bestFriendId this.context.bestFriendId 'null'}}`);

assert.strictEqual(getRootElement().textContent, 'id: 2', 'the id is initially correct');
assert.strictEqual(testContext.bestFriendId, '2', 'the id is initially correct');
user.bestFriend = store.createRecord('user', { name: 'Bill' });
await settled();
assert.strictEqual(getRootElement().textContent, 'id: null', 'the id updates to null');
assert.strictEqual(testContext.bestFriendId, null, 'the id is correct when we swap records');
await user.bestFriend.save();
await settled();
assert.strictEqual(getRootElement().textContent, 'id: 6', 'the id updates when the related record id updates');
assert.strictEqual(testContext.bestFriendId, '6', 'the id is correct when the record is saved');
});

test('HasManyReference.ids() is autotracked', async function (assert) {
class TestContext {
user = user;

get friendIds() {
return this.user.hasMany('friends').ids();
}
}
const testContext = new TestContext();
this.set('context', testContext);
await render(hbs`{{#each this.context.friendIds as |id|}}id: {{if id id 'null'}}, {{/each}}`);

assert.strictEqual(getRootElement().textContent, 'id: 2, ', 'the ids are initially correct');
assert.deepEqual(testContext.friendIds, ['2'], 'the ids are initially correct');
const bill = store.createRecord('user', { name: 'Bill' });
user.friends.pushObject(bill);
await settled();
assert.strictEqual(getRootElement().textContent, 'id: 2, id: null, ', 'the id is added for the new record');
assert.deepEqual(testContext.friendIds, ['2', null], 'the ids are correct when we add a new record');
await bill.save();
await settled();
assert.strictEqual(
getRootElement().textContent,
'id: 2, id: 6, ',
'the id updates when the related record id updates'
);
assert.deepEqual(testContext.friendIds, ['2', '6'], 'the ids are correct when the new record is saved');
});

test('RecordReference.id() is autotracked', async function (assert) {
const dan = store.createRecord('user', { name: 'Dan' });
const identifier = recordIdentifierFor(dan);
const reference = store.getReference(identifier);

class TestContext {
user = reference;

get id() {
return this.user.id();
}
}

const testContext = new TestContext();
this.set('context', testContext);

await render(hbs`id: {{if this.context.id this.context.id 'null'}}`);

assert.strictEqual(getRootElement().textContent, 'id: null', 'the id is null');
assert.strictEqual(testContext.id, null, 'the id is correct initially');
await dan.save();
await settled();
assert.strictEqual(getRootElement().textContent, 'id: 6', 'the id updates when the record id updates');
assert.strictEqual(testContext.id, '6', 'the id is correct when the record is saved');
});
});
}
128 changes: 70 additions & 58 deletions packages/-ember-data/tests/integration/references/has-many-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,27 +361,18 @@ module('integration/references/has-many', function (hooks) {
});
});

testInDebug('push(array) asserts polymorphic type', function (assert) {
testInDebug('push(array) asserts polymorphic type', async function (assert) {
let store = this.owner.lookup('service:store');

var family;
run(function () {
family = store.push({
data: {
type: 'family',
id: 1,
},
});
let family = store.push({
data: {
type: 'family',
id: 1,
},
});
let personsReference = family.hasMany('persons');

var personsReference = family.hasMany('persons');

assert.expectAssertion(() => {
run(() => {
var data = [{ data: { type: 'family', id: 1 } }];

personsReference.push(data);
});
assert.expectAssertion(async () => {
await personsReference.push([{ data: { type: 'family', id: '1' } }]);
}, "The 'family' type does not implement 'person' and thus cannot be assigned to the 'persons' relationship in 'family'. Make it a descendant of 'person' or use a mixin of the same name.");
});

Expand Down Expand Up @@ -429,56 +420,77 @@ module('integration/references/has-many', function (hooks) {
});
});

test('push(promise)', function (assert) {
var done = assert.async();

let store = this.owner.lookup('service:store');
test('push(promise)', async function (assert) {
const store = this.owner.lookup('service:store');
const deferred = defer();

var push;
var deferred = defer();

run(function () {
var family = store.push({
data: {
type: 'family',
id: 1,
relationships: {
persons: {
data: [
{ type: 'person', id: 1 },
{ type: 'person', id: 2 },
],
},
const family = store.push({
data: {
type: 'family',
id: 1,
relationships: {
persons: {
data: [
{ type: 'person', id: 1 },
{ type: 'person', id: 2 },
],
},
},
});
var personsReference = family.hasMany('persons');
push = personsReference.push(deferred.promise);
},
});
const personsReference = family.hasMany('persons');
let pushResult = personsReference.push(deferred.promise);

assert.ok(push.then, 'HasManyReference.push returns a promise');
assert.ok(pushResult.then, 'HasManyReference.push returns a promise');

run(function () {
var payload = {
data: [
{ data: { type: 'person', id: 1, attributes: { name: 'Vito' } } },
{ data: { type: 'person', id: 2, attributes: { name: 'Michael' } } },
],
};
const payload = {
data: [
{ data: { type: 'person', id: 1, attributes: { name: 'Vito' } } },
{ data: { type: 'person', id: 2, attributes: { name: 'Michael' } } },
],
};

deferred.resolve(payload);
});
deferred.resolve(payload);

run(function () {
push.then(function (records) {
assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records');
assert.strictEqual(get(records, 'length'), 2);
assert.strictEqual(records.objectAt(0).get('name'), 'Vito');
assert.strictEqual(records.objectAt(1).get('name'), 'Michael');
const records = await pushResult;
assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records');
assert.strictEqual(get(records, 'length'), 2);
assert.strictEqual(records.objectAt(0).get('name'), 'Vito');
assert.strictEqual(records.objectAt(1).get('name'), 'Michael');
});

done();
});
test('push valid json:api', async function (assert) {
const store = this.owner.lookup('service:store');

const family = store.push({
data: {
type: 'family',
id: 1,
relationships: {
persons: {
data: [
{ type: 'person', id: 1 },
{ type: 'person', id: 2 },
],
},
},
},
});
const personsReference = family.hasMany('persons');
const payload = {
data: [
{ type: 'person', id: 1, attributes: { name: 'Vito' } },
{ type: 'person', id: 2, attributes: { name: 'Michael' } },
],
};
const pushResult = personsReference.push(payload);
assert.ok(pushResult.then, 'HasManyReference.push returns a promise');

const records = await pushResult;
assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records');
assert.strictEqual(get(records, 'length'), 2);
assert.strictEqual(records.objectAt(0).get('name'), 'Vito');
assert.strictEqual(records.objectAt(1).get('name'), 'Michael');
});

test('value() returns null when reference is not yet loaded', function (assert) {
Expand Down
2 changes: 1 addition & 1 deletion packages/store/addon/-private/system/core-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1728,7 +1728,7 @@ abstract class CoreStore extends Service {
if (arguments.length === 1 && isMaybeIdentifier(identifier)) {
let stableIdentifier = identifierCacheFor(this).peekRecordIdentifier(identifier);
if (stableIdentifier) {
return internalModelFactoryFor(this).peek(stableIdentifier)?.getRecord();
return internalModelFactoryFor(this).peek(stableIdentifier)?.getRecord() || null;
}
return null;
}
Expand Down
Loading

0 comments on commit 856f7b3

Please sign in to comment.