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

Feat hasMany linksMode #9617

Open
wants to merge 17 commits into
base: feat-links-mode
Choose a base branch
from
Open
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
30 changes: 28 additions & 2 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { CollectionEdge, Graph, GraphEdge, ImplicitEdge, ResourceEdge } from '@ember-data/graph/-private';
import { graphFor, isBelongsTo, peekGraph } from '@ember-data/graph/-private';
import type Store from '@ember-data/store';
import { isStableIdentifier } from '@ember-data/store/-private';
import type { CacheCapabilitiesManager } from '@ember-data/store/types';
import { LOG_MUTATIONS, LOG_OPERATIONS, LOG_REQUESTS } from '@warp-drive/build-config/debugging';
import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations';
Expand All @@ -29,6 +30,7 @@ import type {
import type {
CollectionField,
FieldSchema,
LegacyHasManyField,
LegacyRelationshipSchema,
ResourceField,
} from '@warp-drive/core-types/schema/fields';
Expand Down Expand Up @@ -237,15 +239,21 @@ export default class JSONAPICache implements Cache {
Counts.set(type, (Counts.get(type) || 0) + 1);
}

let str = `JSON:API Cache - put (${doc.content?.lid || doc.request?.url || 'unknown-request'})\n\tContents:`;
let str = `JSON:API Cache - put (${doc.content?.lid || doc.request?.url || 'unknown-request'})\n\tContent Counts:`;
Counts.forEach((count, type) => {
str += `\n\t\t${type}: ${count}`;
});
if (Counts.size === 0) {
str += `\t(empty)`;
}
// eslint-disable-next-line no-console
console.log(str);
console.log(str, {
lid: doc.content?.lid,
content: structuredClone(doc.content),
// we may need a specialized copy here
request: doc.request, // structuredClone(doc.request),
response: doc.response, // structuredClone(doc.response),
});
}

if (included) {
Expand Down Expand Up @@ -337,6 +345,24 @@ export default class JSONAPICache implements Cache {
this._capabilities.notifyChange(identifier, hasExisting ? 'updated' : 'added');
}

if (doc.request?.op === 'findHasMany') {
const parentIdentifier = doc.request.options?.identifier as StableRecordIdentifier | undefined;
const parentField = doc.request.options?.field as LegacyHasManyField | undefined;
assert(`Expected a hasMany field`, parentField?.kind === 'hasMany');
assert(
`Expected a parent identifier for a findHasMany request`,
parentIdentifier && isStableIdentifier(parentIdentifier)
);
if (parentField && parentIdentifier) {
this.__graph.push({
op: 'updateRelationship',
record: parentIdentifier,
field: parentField.name,
value: resourceDocument,
});
}
}

return resourceDocument;
}

Expand Down
8 changes: 5 additions & 3 deletions packages/json-api/src/-private/validate-document-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ function validateHasManyToLinksMode(
_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`
);
if (field.options.async) {
throw new Error(
`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async hasMany is not yet supported`
);
}
}
2 changes: 1 addition & 1 deletion packages/model/src/-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { Model } from './-private/model';
export type { ModelStore } from './-private/model';
export { Errors } from './-private/errors';

export { RelatedCollection as ManyArray } from './-private/many-array';
export { RelatedCollection as ManyArray } from '@ember-data/store/-private';
export { PromiseBelongsTo } from './-private/promise-belongs-to';
export { PromiseManyArray } from './-private/promise-many-array';

Expand Down
3 changes: 2 additions & 1 deletion packages/model/src/-private/legacy-relationships-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isStableIdentifier,
peekCache,
recordIdentifierFor,
RelatedCollection as ManyArray,
SOURCE,
storeFor,
} from '@ember-data/store/-private';
Expand All @@ -29,7 +30,6 @@ import type {
SingleResourceRelationship,
} from '@warp-drive/core-types/spec/json-api-raw';

import { RelatedCollection as ManyArray } from './many-array';
import type { MinimalLegacyRecord } from './model-methods';
import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to';
import { PromiseBelongsTo } from './promise-belongs-to';
Expand Down Expand Up @@ -258,6 +258,7 @@ export class LegacySupport {
isPolymorphic: definition.isPolymorphic,
isAsync: definition.isAsync,
_inverseIsAsync: definition.inverseIsAsync,
// @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
manager: this,
isLoaded: !definition.isAsync,
allowMutation: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/model.type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import { expectTypeOf } from 'expect-type';

import Store from '@ember-data/store';
import type { RelatedCollection as ManyArray } from '@ember-data/store/-private';
import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields';
import { Type } from '@warp-drive/core-types/symbols';

import { attr } from './attr';
import { belongsTo } from './belongs-to';
import { hasMany } from './has-many';
import type { RelatedCollection as ManyArray } from './many-array';
import { Model } from './model';
import type { PromiseBelongsTo } from './promise-belongs-to';
import type { PromiseManyArray } from './promise-many-array';
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/promise-many-array.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { RelatedCollection as ManyArray } from '@ember-data/store/-private';
import type { BaseFinderOptions } from '@ember-data/store/types';
import { compat } from '@ember-data/tracking';
import { defineSignal } from '@ember-data/tracking/-private';
import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations';
import { assert } from '@warp-drive/build-config/macros';

import type { RelatedCollection as ManyArray } from './many-array';
import { LegacyPromiseProxy } from './promise-belongs-to';

export interface HasManyProxyCreateArgs<T = unknown> {
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/references/has-many.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CollectionEdge, Graph } from '@ember-data/graph/-private';
import type Store from '@ember-data/store';
import type { NotificationType } from '@ember-data/store';
import type { RelatedCollection as ManyArray } from '@ember-data/store/-private';
import type { BaseFinderOptions } from '@ember-data/store/types';
import { cached, compat } from '@ember-data/tracking';
import { defineSignal } from '@ember-data/tracking/-private';
Expand All @@ -22,7 +23,6 @@ import type { IsUnknown } from '../belongs-to';
import { assertPolymorphicType } from '../debug/assert-polymorphic-type';
import type { LegacySupport } from '../legacy-relationships-support';
import { areAllInverseRecordsLoaded, LEGACY_SUPPORT } from '../legacy-relationships-support';
import type { RelatedCollection as ManyArray } from '../many-array';
import type { MaybeHasManyFields } from '../type-utils';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/type-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RelatedCollection } from '@ember-data/store/-private';
import type { TypedRecordInstance } from '@warp-drive/core-types/record';
import type { Type } from '@warp-drive/core-types/symbols';

import type { RelatedCollection } from './many-array';
import type { Model } from './model';
import type { PromiseBelongsTo } from './promise-belongs-to';
import type { PromiseManyArray } from './promise-many-array';
Expand Down
4 changes: 2 additions & 2 deletions packages/model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export { Model as default, attr, belongsTo, hasMany } from './-private';

export type { PromiseBelongsTo as AsyncBelongsTo } from './-private/promise-belongs-to';
export type { PromiseManyArray as AsyncHasMany } from './-private/promise-many-array';
export type { RelatedCollection as ManyArray } from './-private/many-array';
export type { RelatedCollection as HasMany } from './-private/many-array';
export type { RelatedCollection as ManyArray } from '@ember-data/store/-private';
export type { RelatedCollection as HasMany } from '@ember-data/store/-private';
export { instantiateRecord, teardownRecord, modelFor } from './-private/hooks';
export { ModelSchemaProvider } from './-private/schema-provider';
65 changes: 62 additions & 3 deletions packages/schema-record/src/-private/compute.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Future } from '@ember-data/request';
import type Store from '@ember-data/store';
import type { StoreRequestInput } from '@ember-data/store';
import { RelatedCollection as ManyArray } from '@ember-data/store/-private';
import { defineSignal, getSignal, peekSignal } from '@ember-data/tracking/-private';
import { DEBUG } from '@warp-drive/build-config/env';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
Expand All @@ -13,23 +14,25 @@ import type {
DerivedField,
FieldSchema,
GenericField,
LegacyHasManyField,
LocalField,
ObjectField,
SchemaArrayField,
SchemaObjectField,
} from '@warp-drive/core-types/schema/fields';
import type { Link, Links } from '@warp-drive/core-types/spec/json-api-raw';
import type { CollectionResourceRelationship, Link, Links } from '@warp-drive/core-types/spec/json-api-raw';
import { RecordStore } from '@warp-drive/core-types/symbols';

import { SchemaRecord } from '../record';
import type { SchemaService } from '../schema';
import { Editable, Identifier, Legacy, Parent } from '../symbols';
import { ManagedArray } from './managed-array';
import { ManagedObject } from './managed-object';
import { ManyArrayManager } from './many-array-manager';

export const ManagedArrayMap = getOrSetGlobal(
'ManagedArrayMap',
new Map<SchemaRecord, Map<FieldSchema, ManagedArray>>()
new Map<SchemaRecord, Map<FieldSchema, ManagedArray | ManyArray>>()
);
export const ManagedObjectMap = getOrSetGlobal(
'ManagedObjectMap',
Expand All @@ -47,7 +50,7 @@ export function computeLocal(record: typeof Proxy<SchemaRecord>, field: LocalFie
return signal.lastValue;
}

export function peekManagedArray(record: SchemaRecord, field: FieldSchema): ManagedArray | undefined {
export function peekManagedArray(record: SchemaRecord, field: FieldSchema): ManagedArray | ManyArray | undefined {
const managedArrayMapForRecord = ManagedArrayMap.get(record);
if (managedArrayMapForRecord) {
return managedArrayMapForRecord.get(field);
Expand Down Expand Up @@ -319,3 +322,59 @@ export function computeResource<T extends SchemaRecord>(

return new ResourceRelationship<T>(store, cache, parent, identifier, field, prop);
}

export function computeHasMany(
store: Store,
schema: SchemaService,
cache: Cache,
record: SchemaRecord,
identifier: StableRecordIdentifier,
field: LegacyHasManyField,
path: string[],
editable: boolean,
legacy: boolean
) {
// the thing we hand out needs to know its owner and path in a private manner
// its "address" is the parent identifier (identifier) + field name (field.name)
// in the nested object case field name here is the full dot path from root resource to this value
// its "key" is the field on the parent record
// its "owner" is the parent record

const managedArrayMapForRecord = ManagedArrayMap.get(record);
let managedArray;
if (managedArrayMapForRecord) {
managedArray = managedArrayMapForRecord.get(field);
}
if (managedArray) {
return managedArray;
} else {
const rawValue = cache.getRelationship(identifier, field.name) as CollectionResourceRelationship;
if (!rawValue) {
return null;
}
managedArray = new ManyArray<unknown>({
store,
type: field.type,
identifier,
cache,
identifiers: rawValue.data as StableRecordIdentifier[],
key: field.name,
meta: rawValue.meta || null,
links: rawValue.links || null,
isPolymorphic: field.options.polymorphic ?? false,
isAsync: field.options.async ?? false,
// TODO: Grab the proper value
_inverseIsAsync: false,
// @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
manager: new ManyArrayManager(record),
isLoaded: true,
allowMutation: editable,
});
if (!managedArrayMapForRecord) {
ManagedArrayMap.set(record, new Map([[field, managedArray]]));
} else {
managedArrayMapForRecord.set(field, managedArray);
}
}
return managedArray;
}
98 changes: 98 additions & 0 deletions packages/schema-record/src/-private/many-array-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type Store from '@ember-data/store';
import type { RelatedCollection as ManyArray } from '@ember-data/store/-private';
import { fastPush, SOURCE } from '@ember-data/store/-private';
import { assert } from '@warp-drive/build-config/macros';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
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 { CacheOptions } from '@warp-drive/core-types/request';
import { EnableHydration } from '@warp-drive/core-types/request';
import type { CollectionResourceRelationship } from '@warp-drive/core-types/spec/json-api-raw';
import { RecordStore } from '@warp-drive/core-types/symbols';

import type { SchemaRecord } from '../record';
import { Identifier } from '../symbols';

export interface FindHasManyOptions {
reload?: boolean;
backgroundReload?: boolean;
}

export class ManyArrayManager {
declare record: SchemaRecord;
declare store: Store;
declare cache: Cache;
declare identifier: StableRecordIdentifier;

constructor(record: SchemaRecord) {
this.record = record;
this.store = record[RecordStore];
this.identifier = record[Identifier];
}

_syncArray(array: ManyArray) {
const rawValue = this.store.cache.getRelationship(this.identifier, array.key) as CollectionRelationship;

if (rawValue.meta) {
array.meta = rawValue.meta;
}

if (rawValue.links) {
array.links = rawValue.links;
}

const currentState = array[SOURCE];
currentState.length = 0;
fastPush(currentState, rawValue.data as StableRecordIdentifier[]);
}

reloadHasMany<T>(key: string, options?: FindHasManyOptions): Promise<ManyArray<T>> {
const field = this.store.schema.fields(this.identifier).get(key);
assert(`Expected a hasMany field for ${key}`, field?.kind === 'hasMany');

const cacheOptions = options ? extractCacheOptions(options) : { reload: true };
cacheOptions.types = [field.type];

const rawValue = this.store.cache.getRelationship(this.identifier, key) as CollectionRelationship;

const req = {
url: getRelatedLink(rawValue),
op: 'findHasMany',
method: 'GET' as const,
records: rawValue.data as StableRecordIdentifier[],
cacheOptions,
options: {
field,
identifier: this.identifier,
links: rawValue.links,
meta: rawValue.meta,
},
[EnableHydration]: false,
};

return this.store.request(req) as unknown as Promise<ManyArray<T>>;
}

mutate(mutation: LocalRelationshipOperation): void {
this.cache.mutate(mutation);
}
}

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

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

function extractCacheOptions(options: FindHasManyOptions) {
const cacheOptions: CacheOptions = {};
if ('reload' in options) {
cacheOptions.reload = options.reload;
}
if ('backgroundReload' in options) {
cacheOptions.backgroundReload = options.backgroundReload;
}
return cacheOptions;
}
Loading
Loading