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 8 commits into
base: feat-links-mode
Choose a base branch
from
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;
}
42 changes: 42 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,42 @@
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 type { BaseFinderOptions } from '@ember-data/store/types';
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 { RecordStore } from '@warp-drive/core-types/symbols';

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

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?: BaseFinderOptions): Promise<ManyArray<T>> {}
}
28 changes: 28 additions & 0 deletions packages/schema-record/src/record.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 { MinimalLegacyRecord } from '@ember-data/model/-private/model-methods';
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 { recordIdentifierFor, setRecordIdentifier } from '@ember-data/store/-private';
import { addToTransaction, entangleSignal, getSignal, type Signal, Signals } from '@ember-data/tracking/-private';
import { assert } from '@warp-drive/build-config/macros';
Expand All @@ -18,6 +19,7 @@ import {
computeAttribute,
computeDerivation,
computeField,
computeHasMany,
computeLocal,
computeObject,
computeResource,
Expand Down Expand Up @@ -328,6 +330,21 @@ export class SchemaRecord {
entangleSignal(signals, receiver, field.name);
return getLegacySupport(receiver as unknown as MinimalLegacyRecord).getBelongsTo(field.name);
case 'hasMany':
if (field.options.linksMode) {
entangleSignal(signals, receiver, field.name);

return computeHasMany(
store,
schema,
cache,
target,
identifier,
field,
propArray,
Mode[Editable],
Mode[Legacy]
);
}
if (!HAS_MODEL_PACKAGE) {
assert(
`Cannot use hasMany fields in your schema unless @ember-data/model is installed to provide legacy model support. ${field.name} should likely be migrated to be a collection field.`
Expand Down Expand Up @@ -616,6 +633,17 @@ export class SchemaRecord {
} else if (field.kind === 'resource') {
// FIXME
} else if (field.kind === 'hasMany') {
if (field.options.linksMode) {
const peeked = peekManagedArray(self, field) as ManyArray | undefined;
if (peeked) {
// const arrSignal = peeked[ARRAY_SIGNAL];
// arrSignal.shouldReset = true;
// addToTransaction(arrSignal);
peeked.notify();
}
return;
}

assert(`Expected to have a getLegacySupport function`, getLegacySupport);
assert(`Can only use hasMany fields when the resource is in legacy mode`, Mode[Legacy]);

Expand Down
1 change: 1 addition & 0 deletions packages/store/src/-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ export { setRecordIdentifier, StoreMap } from './-private/caches/instance-cache'
export { setCacheFor } from './-private/caches/cache-utils';
export { normalizeModelName as _deprecatingNormalize } from './-private/utils/normalize-model-name';
export type { StoreRequestInput } from './-private/cache-handler/handler';
export { RelatedCollection } from './-private/record-arrays/many-array';
5 changes: 5 additions & 0 deletions packages/store/src/-private/managers/record-array-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { addTransactionCB } from '@ember-data/tracking/-private';
import { getOrSetGlobal } from '@warp-drive/core-types/-private';
import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph';
import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier';
import type { ImmutableRequestInfo } from '@warp-drive/core-types/request';
import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/json-api-raw';
Expand Down Expand Up @@ -131,6 +132,10 @@ export class RecordArrayManager {
this._pending.delete(array);
}

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

/**
Get the `RecordArray` for a modelName, which contains all loaded records of
given modelName.
Expand Down
16 changes: 14 additions & 2 deletions packages/store/src/-private/record-arrays/identifier-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
} from '@ember-data/tracking/-private';
import { assert } from '@warp-drive/build-config/macros';
import { getOrSetGlobal } from '@warp-drive/core-types/-private';
import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph';
import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier';
import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record';
import type { ImmutableRequestInfo } from '@warp-drive/core-types/request';
import type { Links, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw';

import type { OpaqueRecordInstance } from '../../-types/q/record-instance';
import type { BaseFinderOptions } from '../../types';
import { isStableIdentifier } from '../caches/identifier-cache';
import { recordIdentifierFor } from '../caches/instance-cache';
import type { RecordArrayManager } from '../managers/record-array-manager';
Expand Down Expand Up @@ -125,8 +127,18 @@ function safeForEach<T>(
return instance;
}

type MinimumManager = {
type PromiseTo<T> = Omit<Promise<T>, typeof Symbol.toStringTag>;

type PromiseManyArray<T> = {
length: number;
content: IdentifierArray<T> | null;
promise: Promise<IdentifierArray<T>> | null;
} & PromiseTo<IdentifierArray<T>>;

export type MinimumManager = {
_syncArray: (array: IdentifierArray) => void;
mutate?(mutation: LocalRelationshipOperation): void;
reloadHasMany?<T>(key: string, options?: BaseFinderOptions): Promise<IdentifierArray<T>> | PromiseManyArray<T>;
};

/**
Expand Down Expand Up @@ -432,7 +444,7 @@ export class IdentifierArray<T = unknown> {
},

getPrototypeOf() {
return IdentifierArray.prototype;
return Array.prototype as unknown as IdentifierArray<T>;
},
}) as IdentifierArray<T>;

Expand Down
Loading
Loading