diff --git a/packages/graph/src/-private/operations/update-relationship.ts b/packages/graph/src/-private/operations/update-relationship.ts index 96fd82f7887..07da6931fb5 100644 --- a/packages/graph/src/-private/operations/update-relationship.ts +++ b/packages/graph/src/-private/operations/update-relationship.ts @@ -115,6 +115,11 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela hasUpdatedLink = true; } } + + assert( + `Cannot fetch ${identifier?.type ?? 'unknown'}.${op.field} because the field is in linksMode but the response includes no links`, + !definition.isLinksMode || (payload.links && payload.links.related) + ); } /* diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 9ab95958189..6b54fb23510 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -207,7 +207,7 @@ export default class JSONAPICache implements Cache { } else if (isMetaDocument(doc)) { return this._putDocument(doc, undefined, undefined); } - + // TODO: Check that the payload has the related documents const jsonApiDoc = doc.content as SingleResourceDocument | CollectionResourceDocument; const included = jsonApiDoc.included; let i: number, length: number; diff --git a/tests/main/tests/integration/relationships/links-mode/belongs-to-test.ts b/tests/main/tests/integration/relationships/links-mode/belongs-to-test.ts index ee92ab68620..8a9abdc927c 100644 --- a/tests/main/tests/integration/relationships/links-mode/belongs-to-test.ts +++ b/tests/main/tests/integration/relationships/links-mode/belongs-to-test.ts @@ -109,6 +109,88 @@ module('integration/relationship/belongs-to BelongsTo Relationships (linksMode)' assert.strictEqual(record.bestFriend?.id, '3', 'bestFriend.id is correct'); assert.strictEqual(record.bestFriend?.name, 'Ray', 'bestFriend.name is correct'); }); + + test('belongsTo reload fails if no links in response in linksMode', async function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const manager = new RequestManager(); + const handler: Handler = { + request(context): Promise { + assert.step(`op=${context.request.op ?? 'UNKNOWN OP CODE'}, url=${context.request.url ?? 'UNKNOWN URL'}`); + return Promise.resolve({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Ray', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + } as T); + }, + }; + const InterceptingHandler: Handler = { + request(context, next) { + assert.step('LegacyNetworkHandler.request was called'); + return LegacyNetworkHandler.request(context, next); + }, + }; + + manager.use([InterceptingHandler, handler]); + manager.useCache(CacheHandler); + store.requestManager = manager; + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + links: { related: '/user/1/bestFriend' }, + data: { type: 'user', id: '2' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + bestFriend: { + links: { related: '/user/2/bestFriend' }, + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.bestFriend?.id, '2', 'bestFriend.id is accessible'); + assert.strictEqual(record.bestFriend?.name, 'Rey', 'bestFriend.name is accessible'); + assert.strictEqual(record.bestFriend?.bestFriend?.id, record.id, 'bestFriend is reciprocal'); + + await assert.expectAssertion( + () => record.belongsTo('bestFriend').reload(), + 'Cannot fetch user.bestFriend because the field is in linksMode but the response includes no links' + ); + + assert.verifySteps( + ['LegacyNetworkHandler.request was called', 'op=findBelongsTo, url=/user/1/bestFriend'], + 'op and url are correct' + ); + }); }); // TODO: a second thing to do for legacy: diff --git a/tests/warp-drive__schema-record/tests/legacy/reads/relationships-test.ts b/tests/warp-drive__schema-record/tests/legacy/reads/relationships-test.ts index 32ff0ede84f..40ac9b3ee33 100644 --- a/tests/warp-drive__schema-record/tests/legacy/reads/relationships-test.ts +++ b/tests/warp-drive__schema-record/tests/legacy/reads/relationships-test.ts @@ -408,4 +408,112 @@ module('Legacy | Reads | relationships', function (hooks) { assert.strictEqual(record.bestFriend?.id, '3', 'bestFriend.id is correct'); assert.strictEqual(record.bestFriend?.name, 'Ray', 'bestFriend.name is correct'); }); + + test('sync belongsTo reload will error if no links in response in linksMode', async function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerLegacyDerivations(schema); + + type LegacyUser = WithLegacyDerivations<{ + [Type]: 'user'; + id: string; + name: string; + bestFriend: LegacyUser | null; + }>; + + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'belongsTo', + options: { inverse: 'bestFriend', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + links: { related: '/user/1/bestFriend' }, + data: { type: 'user', id: '2' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + bestFriend: { + links: { related: '/user/2/bestFriend' }, + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is correct'); + assert.strictEqual(record.name, 'Chris', 'name is correct'); + assert.strictEqual(record.bestFriend?.id, '2', 'bestFriend.id is correct'); + assert.strictEqual(record.bestFriend?.name, 'Rey', 'bestFriend.name is correct'); + + const manager = new RequestManager(); + const handler: Handler = { + request(context: RequestContext, next: NextFn): Promise { + assert.step(`op=${context.request.op ?? 'UNKNOWN OP CODE'}, url=${context.request.url ?? 'UNKNOWN URL'}`); + return Promise.resolve({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Ray', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + } as T); + }, + }; + manager.use([handler]); + manager.useCache(CacheHandler); + store.requestManager = manager; + + await assert.expectAssertion( + () => record.belongsTo('bestFriend').reload(), + 'Cannot fetch user.bestFriend because the field is in linksMode but the response includes no links' + ); + + assert.verifySteps(['op=findBelongsTo, url=/user/1/bestFriend'], 'op and url are correct'); + }); }); + +/* +FIXME: +link but no data key = not supported YET +link + data: null = supported +link + data with identifier + referenced record in same payload = supported + referenced record not in payload = not supported +*/ diff --git a/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts b/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts index 68250cbd92e..97eb9e49490 100644 --- a/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts @@ -66,6 +66,7 @@ module('Reads | belongsTo in linksMode', function (hooks) { }, relationships: { bestFriend: { + links: { related: '/user/2/bestFriend' }, data: { type: 'user', id: '1' }, }, }, @@ -82,6 +83,164 @@ module('Reads | belongsTo in linksMode', function (hooks) { assert.strictEqual(record.bestFriend?.bestFriend?.id, record.id, 'bestFriend is reciprocal'); }); + test('we can update sync belongsTo in linksMode', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'belongsTo', + options: { inverse: 'bestFriend', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + links: { related: '/user/1/bestFriend' }, + data: { type: 'user', id: '2' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.bestFriend?.id, '2', 'bestFriend.id is accessible'); + assert.strictEqual(record.bestFriend?.name, 'Rey', 'bestFriend.name is accessible'); + + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + links: { related: '/user/1/bestFriend' }, + data: { type: 'user', id: '3' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Ray', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.bestFriend?.id, '3', 'bestFriend.id is accessible'); + assert.strictEqual(record.bestFriend?.name, 'Ray', 'bestFriend.name is accessible'); + }); + + test('we error in linksMode if the relationship does not include a link', async function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'belongsTo', + options: { inverse: 'bestFriend', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + + await assert.expectAssertion( + () => record.bestFriend, + 'Cannot fetch user.bestFriend because the field is in linksMode but the response includes no links' + ); + }); + test('we error for async belongsTo access in linksMode because we are not implemented yet', async function (this: TestContext, assert) { const store = this.owner.lookup('service:store') as Store; const { schema } = store;