Skip to content

Commit

Permalink
Error if linksMode but no links
Browse files Browse the repository at this point in the history
  • Loading branch information
gitKrystan committed Nov 11, 2024
1 parent ce3a2d5 commit f802284
Show file tree
Hide file tree
Showing 5 changed files with 355 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/graph/src/-private/operations/update-relationship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

/*
Expand Down
2 changes: 1 addition & 1 deletion packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(context): Promise<T> {
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<User>({
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LegacyUser>({
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<T>(context: RequestContext, next: NextFn<T>): Promise<T> {
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
*/
159 changes: 159 additions & 0 deletions tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module('Reads | belongsTo in linksMode', function (hooks) {
},
relationships: {
bestFriend: {
links: { related: '/user/2/bestFriend' },
data: { type: 'user', id: '1' },
},
},
Expand All @@ -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<User>({
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<User>({
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<User>({
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;
Expand Down

0 comments on commit f802284

Please sign in to comment.