Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: stub out linksMode work #9585

Merged
merged 21 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core-types/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ export type RequestInfo<T = unknown, RT = unknown> = Request & {
options?: Record<string, unknown>;

[RequestSignature]?: RT;

[EnableHydration]?: boolean;
};

/**
Expand Down
68 changes: 68 additions & 0 deletions packages/core-types/src/schema/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,40 @@ export type LegacyBelongsToField = {
*/
polymorphic?: boolean;

/**
* Whether this field should ever make use of the legacy support infra
* from @ember-data/model and the LegacyNetworkMiddleware for adapters and serializers.
*
* When true, none of the legacy support will be utilized. Sync relationships
* will be expected to already have all their data. When reloading a sync relationship
* you would be expected to have a `related link` available from a prior relationship
* payload e.g.
*
* ```ts
* {
* data: {
* type: 'user',
* id: '2',
* attributes: { name: 'Chris' },
* relationships: {
* bestFriend: {
* links: { related: "/users/1/bestFriend" },
* data: { type: 'user', id: '1' },
* }
* }
* },
* included: [
* { type: 'user', id: '1', attributes: { name: 'Krystan' } }
* ]
* }
* ```
*
* Async relationships will be loaded via their link if needed.
*
* @typedoc
*/
linksMode?: true;

/**
* When omitted, the cache data for this field will
* clear local state of all changes except for the
Expand Down Expand Up @@ -819,6 +853,40 @@ export type LegacyHasManyField = {
*/
polymorphic?: boolean;

/**
* Whether this field should ever make use of the legacy support infra
* from @ember-data/model and the LegacyNetworkMiddleware for adapters and serializers.
*
* When true, none of the legacy support will be utilized. Sync relationships
* will be expected to already have all their data. When reloading a sync relationship
* you would be expected to have a `related link` available from a prior relationship
* payload e.g.
*
* ```ts
* {
* data: {
* type: 'user',
* id: '2',
* attributes: { name: 'Chris' },
* relationships: {
* bestFriends: {
* links: { related: "/users/1/bestFriends" },
* data: [ { type: 'user', id: '1' } ],
* }
* }
* },
* included: [
* { type: 'user', id: '1', attributes: { name: 'Krystan' } }
* ]
* }
* ```
*
* Async relationships will be loaded via their link if needed.
*
* @typedoc
*/
linksMode?: true;

/**
* When omitted, the cache data for this field will
* clear local state of all changes except for the
Expand Down
2 changes: 1 addition & 1 deletion packages/ember/src/-private/request.gts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
);
}

const wasStoreRequest = (request as { [EnableHydration]: boolean })[EnableHydration] === true;
const wasStoreRequest = request[EnableHydration] === true;
assert(
`Cannot supply a different store via context than was used to create the request`,
!request.store || request.store === this.store
Expand Down
15 changes: 10 additions & 5 deletions packages/graph/src/-private/-edge-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface UpgradedMeta {
isCollection: boolean;
isPolymorphic: boolean;
resetOnRemoteUpdate: boolean;
isLinksMode: boolean;

inverseKind: 'implicit' | RelationshipFieldKind;
/**
Expand All @@ -140,6 +141,7 @@ export interface UpgradedMeta {
inverseIsImplicit: boolean;
inverseIsCollection: boolean;
inverseIsPolymorphic: boolean;
inverseIsLinksMode: boolean;
}

export interface EdgeDefinition {
Expand Down Expand Up @@ -195,6 +197,7 @@ function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) {
definition.inverseIsCollection = inverseDefinition.isCollection;
definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic;
definition.inverseIsImplicit = inverseDefinition.isImplicit;
definition.inverseIsLinksMode = inverseDefinition.isLinksMode;
const resetOnRemoteUpdate =
definition.resetOnRemoteUpdate === false || inverseDefinition.resetOnRemoteUpdate === false ? false : true;
definition.resetOnRemoteUpdate = resetOnRemoteUpdate;
Expand All @@ -215,18 +218,20 @@ function upgradeMeta(meta: RelationshipField): UpgradedMeta {
niceMeta.isImplicit = false;
niceMeta.isCollection = meta.kind === 'hasMany';
niceMeta.isPolymorphic = options && !!options.polymorphic;
niceMeta.isLinksMode = options.linksMode ?? false;

niceMeta.inverseKey = (options && options.inverse) || STR_LATER;
niceMeta.inverseType = STR_LATER;
niceMeta.inverseIsAsync = BOOL_LATER;
niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER;
niceMeta.inverseIsCollection = BOOL_LATER;
niceMeta.inverseIsLinksMode = BOOL_LATER;

niceMeta.resetOnRemoteUpdate = isLegacyField(meta)
? meta.options?.resetOnRemoteUpdate === false
? false
: true
: false;
// prettier-ignore
niceMeta.resetOnRemoteUpdate = !isLegacyField(meta) ? false
gitKrystan marked this conversation as resolved.
Show resolved Hide resolved
: meta.options?.linksMode ? false
: meta.options?.resetOnRemoteUpdate === false ? false
: true;

return niceMeta;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/graph/src/-private/debug/assert-polymorphic-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,15 @@ if (DEBUG) {
kind: definition.inverseKind,
isAsync: definition.inverseIsAsync,
isPolymorphic: true,
isLinksMode: definition.isLinksMode,
isCollection: definition.inverseIsCollection,
isImplicit: definition.inverseIsImplicit,
inverseKey: definition.key,
inverseType: definition.type,
inverseKind: definition.kind,
inverseIsAsync: definition.isAsync,
inverseIsPolymorphic: definition.isPolymorphic,
inverseIsLinksMode: definition.inverseIsLinksMode,
inverseIsImplicit: definition.isImplicit,
inverseIsCollection: definition.isCollection,
resetOnRemoteUpdate: definition.resetOnRemoteUpdate,
Expand Down
11 changes: 6 additions & 5 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import type {
SingleResourceRelationship,
} from '@warp-drive/core-types/spec/json-api-raw';

import { validateDocumentFields } from './validate-document-fields';

type IdentifierCache = Store['identifierCache'];
type InternalCapabilitiesManager = CacheCapabilitiesManager & { _store: Store };

Expand Down Expand Up @@ -213,6 +215,10 @@ export default class JSONAPICache implements Cache {
let i: number, length: number;
const { identifierCache } = this._capabilities;

if (DEBUG) {
validateDocumentFields(this._capabilities.schema, jsonApiDoc);
}

if (LOG_REQUESTS) {
const Counts = new Map();
if (included) {
Expand Down Expand Up @@ -270,11 +276,6 @@ export default class JSONAPICache implements Cache {
);
}

assert(
`Expected a resource object in the 'data' property in the document provided to the cache, but was ${typeof jsonApiDoc.data}`,
typeof jsonApiDoc.data === 'object'
);

const identifier = putOne(this, identifierCache, jsonApiDoc.data);
return this._putDocument(
doc as StructuredDataDocument<SingleResourceDocument>,
Expand Down
141 changes: 141 additions & 0 deletions packages/json-api/src/-private/validate-document-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { SchemaService } from '@ember-data/store/types';
import type { LegacyBelongsToField, LegacyHasManyField } from '@warp-drive/core-types/schema/fields';
import type {
CollectionResourceDocument,
ExistingResourceIdentifierObject,
ExistingResourceObject,
InnerRelationshipDocument,
SingleResourceDocument,
} from '@warp-drive/core-types/spec/json-api-raw';

export function validateDocumentFields(
schema: SchemaService,
jsonApiDoc: SingleResourceDocument | CollectionResourceDocument
) {
const { data, included } = jsonApiDoc;
if (data === null) {
return;
}

if (typeof jsonApiDoc.data !== 'object') {
throw new Error(
`Expected a resource object in the 'data' property in the document provided to the cache, but was ${typeof jsonApiDoc.data}`
);
}

if (Array.isArray(data)) {
for (const resource of data) {
validateResourceFields(schema, resource, { verifyIncluded: true, included });
}
} else {
validateResourceFields(schema, data, { verifyIncluded: true, included });
}

if (included) {
for (const resource of included) {
validateResourceFields(schema, resource, { verifyIncluded: false });
}
}
}

type ValidateResourceFieldsOptions =
| {
verifyIncluded: true;
included: ExistingResourceObject[] | undefined;
}
| {
verifyIncluded: false;
};

function validateResourceFields(
schema: SchemaService,
resource: ExistingResourceObject,
options: ValidateResourceFieldsOptions
) {
if (!resource.relationships) {
return;
}

const resourceType = resource.type;
const fields = schema.fields({ type: resource.type });
for (const [type, relationshipDoc] of Object.entries(resource.relationships)) {
const field = fields.get(type);
if (!field) {
return;
}
switch (field.kind) {
case 'belongsTo': {
if (field.options.linksMode) {
validateBelongsToLinksMode(resourceType, field, relationshipDoc, options);
}
break;
}
case 'hasMany': {
if (field.options.linksMode) {
validateHasManyToLinksMode(resourceType, field, relationshipDoc, options);
}
break;
}
default:
break;
}
}
}

function validateBelongsToLinksMode(
resourceType: string,
field: LegacyBelongsToField,
relationshipDoc: InnerRelationshipDocument<ExistingResourceIdentifierObject>,
options: ValidateResourceFieldsOptions
) {
if (field.options.async) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async is not yet supported`
);
}

if (!relationshipDoc.links?.related) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related link is missing`
);
}

const relationshipData = relationshipDoc.data;
if (Array.isArray(relationshipData)) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the relationship data for a belongsTo relationship is unexpectedly an array`
);
}
// Explicitly allow `null`! Missing key or `undefined` are always invalid.
if (relationshipData === undefined) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the relationship data is undefined`
);
}
if (relationshipData === null) {
return;
}

if (!options.verifyIncluded) {
return;
}
const includedDoc = options.included?.find(
(doc) => doc.type === relationshipData.type && doc.id === relationshipData.id
);
if (!includedDoc) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related data is not included`
);
}
}

function validateHasManyToLinksMode(
resourceType: string,
field: LegacyHasManyField,
_relationshipDoc: InnerRelationshipDocument<ExistingResourceIdentifierObject>,
_options: ValidateResourceFieldsOptions
) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but hasMany is not yet supported`
);
}
1 change: 1 addition & 0 deletions packages/model/src/-private/belongs-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type RelationshipOptions<T, Async extends boolean> = {
inverse: null | (IsUnknown<T> extends true ? string : keyof NoNull<T> & string);
polymorphic?: boolean;
as?: string;
linksMode?: true;
resetOnRemoteUpdate?: boolean;
};

Expand Down
Loading
Loading