Skip to content

Commit

Permalink
Implement legacy relationship support + linksMode reload
Browse files Browse the repository at this point in the history
  • Loading branch information
gitKrystan committed Nov 8, 2024
1 parent 6534d1c commit 1f160e9
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 17 deletions.
3 changes: 3 additions & 0 deletions packages/core-types/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ export type RequestInfo<T = unknown, RT = unknown> = Request & {
options?: Record<string, unknown>;

[RequestSignature]?: RT;

// FIXME: Shouldn't this remain private?
[EnableHydration]?: boolean;
};

/**
Expand Down
58 changes: 45 additions & 13 deletions packages/model/src/-private/legacy-relationships-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dependencySatisfies, importSync, macroCondition } from '@embroider/macr
import type { CollectionEdge, Graph, GraphEdge, ResourceEdge, UpgradedMeta } from '@ember-data/graph/-private';
import { upgradeStore } from '@ember-data/legacy-compat/-private';
import type Store from '@ember-data/store';
import type { Document } from '@ember-data/store';
import type { LiveArray } from '@ember-data/store/-private';
import {
fastPush,
Expand All @@ -21,6 +22,7 @@ import type { Cache } from '@warp-drive/core-types/cache';
import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship';
import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph';
import type { OpaqueRecordInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record';
import { EnableHydration } from '@warp-drive/core-types/request';
import type {
CollectionResourceRelationship,
InnerRelationshipDocument,
Expand Down Expand Up @@ -470,12 +472,21 @@ export class LegacySupport {
assert(`Expected collection to be an array`, !identifiers || Array.isArray(identifiers));
assert(`Expected stable identifiers`, !identifiers || identifiers.every(isStableIdentifier));

return this.store.request({
op: 'findHasMany',
records: identifiers || [],
data: request,
cacheOptions: { [Symbol.for('wd:skip-cache')]: true },
}) as unknown as Promise<void>;
const req = field.options.linksMode
? {
url: getRelatedLink(resource),
op: 'findHasMany',
method: 'GET' as const,
records: identifiers || [],
data: request,
[EnableHydration]: false,
}
: {
records: identifiers || [],
data: request,
cacheOptions: { [Symbol.for('wd:skip-cache')]: true },
};
return this.store.request(req) as unknown as Promise<void>;
}

const preferLocalCache = hasReceivedData && !isEmpty;
Expand Down Expand Up @@ -552,14 +563,28 @@ export class LegacySupport {

// fetch via link
if (shouldFindViaLink) {
const future = this.store.request<StableRecordIdentifier | null>({
op: 'findBelongsTo',
records: identifier ? [identifier] : [],
data: request,
cacheOptions: { [Symbol.for('wd:skip-cache')]: true },
});
const req = field.options.linksMode
? {
url: getRelatedLink(resource),
op: 'findBelongsTo',
method: 'GET' as const,
records: identifier ? [identifier] : [],
data: request,
[EnableHydration]: false,
}
: {
op: 'findBelongsTo',
records: identifier ? [identifier] : [],
data: request,
cacheOptions: { [Symbol.for('wd:skip-cache')]: true },
};
const future = this.store.request<StableRecordIdentifier | null>(req);
this._pending[key] = future
.then((doc) => doc.content)
.then((doc) =>
field.options.linksMode
? (doc.content as unknown as Document<StableRecordIdentifier | null>).data!
: doc.content
)
.finally(() => {
this._pending[key] = undefined;
});
Expand Down Expand Up @@ -634,6 +659,13 @@ export class LegacySupport {
}
}

function getRelatedLink(resource: SingleResourceRelationship | CollectionResourceRelationship): string {
const related = resource.links?.related;
assert(`Expected a related link`, related);

return typeof related === 'object' ? related.href : related;
}

function handleCompletedRelationshipRequest(
recordExt: LegacySupport,
key: string,
Expand Down
2 changes: 0 additions & 2 deletions packages/model/src/-private/references/belongs-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,6 @@ export default class BelongsToReference<
@param {Object} options the options to pass in.
@return {Promise} a promise that resolves with the record in this belongs-to relationship.
*/
// load and reload on the references will need to understand linksmode and make a `findBelongsTo`
// or `findHasMany` operation request with the link instead of getBelongsto/reloadBelongsTo etc
async load(options?: Record<string, unknown>): Promise<Related | null> {
const support: LegacySupport = (LEGACY_SUPPORT as Map<StableRecordIdentifier, LegacySupport>).get(
this.___identifier
Expand Down
7 changes: 7 additions & 0 deletions packages/model/src/migration-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { assert } from '@warp-drive/build-config/macros';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import { getOrSetGlobal } from '@warp-drive/core-types/-private';
import type { ObjectValue } from '@warp-drive/core-types/json/raw';
import type { TypedRecordInstance } from '@warp-drive/core-types/record';
import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts';
import type {
ArrayField,
Expand Down Expand Up @@ -37,6 +38,12 @@ import {
import RecordState from './-private/record-state';
import { buildSchema } from './hooks';

export type WithLegacyDerivations<T extends TypedRecordInstance> = T &
MinimalLegacyRecord & {
belongsTo: typeof belongsTo;
hasMany: typeof hasMany;
};

type AttributesSchema = ReturnType<Exclude<SchemaService['attributesDefinitionFor'], undefined>>;
type RelationshipsSchema = ReturnType<Exclude<SchemaService['relationshipsDefinitionFor'], undefined>>;

Expand Down
4 changes: 2 additions & 2 deletions packages/store/src/-private/store-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,11 +742,11 @@ export class Store extends BaseClass {
const opts: {
store: Store;
disableTestWaiter?: boolean;
[EnableHydration]: true;
[EnableHydration]: boolean;
records?: StableRecordIdentifier[];
} = {
store: this,
[EnableHydration]: true,
[EnableHydration]: requestConfig[EnableHydration] ?? true,
};

if (requestConfig.records) {
Expand Down
112 changes: 112 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 @@ -4,7 +4,16 @@ import { module, test } from 'qunit';

import { setupTest } from 'ember-qunit';

import {
registerDerivations as registerLegacyDerivations,
withDefaults as withLegacy,
type WithLegacyDerivations,
} from '@ember-data/model/migration-support';
import type { Handler, NextFn } from '@ember-data/request';
import RequestManager from '@ember-data/request';
import type Store from '@ember-data/store';
import { CacheHandler } from '@ember-data/store';
import type { RequestContext } from '@warp-drive/core-types/request';
import type { Type } from '@warp-drive/core-types/symbols';
import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema';

Expand Down Expand Up @@ -52,6 +61,7 @@ module('Reads | belongsTo in linksMode', function (hooks) {
},
relationships: {
bestFriend: {
links: { related: '/user/1/bestFriend' },
data: { type: 'user', id: '2' },
},
},
Expand Down Expand Up @@ -146,4 +156,106 @@ module('Reads | belongsTo in linksMode', function (hooks) {
'Cannot fetch user.bestFriend because the field is in linksMode but async is not yet supported'
);
});

test('in legacy relationship support, we can reload sync belongsTo 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: {
links: { related: '/user/3/bestFriend' },
data: { type: 'user', id: '1' },
},
},
},
} as T);
},
};
manager.use([handler]);
manager.useCache(CacheHandler);
store.requestManager = manager;

await record.belongsTo('bestFriend').reload();

assert.verifySteps(['op=findBelongsTo, url=/user/1/bestFriend'], 'op and url are correct');

assert.strictEqual(record.id, '1', 'id is correct');
assert.strictEqual(record.name, 'Chris', 'name is correct');
assert.strictEqual(record.bestFriend?.id, '3', 'bestFriend.id is correct');
assert.strictEqual(record.bestFriend?.name, 'Ray', 'bestFriend.name is correct');
});
});

0 comments on commit 1f160e9

Please sign in to comment.