diff --git a/packages/json-api/src/-private/validate-document-fields.ts b/packages/json-api/src/-private/validate-document-fields.ts index b077a54116a..a6edebec6ab 100644 --- a/packages/json-api/src/-private/validate-document-fields.ts +++ b/packages/json-api/src/-private/validate-document-fields.ts @@ -135,7 +135,9 @@ function validateHasManyToLinksMode( _relationshipDoc: InnerRelationshipDocument, _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` + ); + } } diff --git a/packages/model/src/-private.ts b/packages/model/src/-private.ts index 92b7d2d2c7e..e7112491ede 100644 --- a/packages/model/src/-private.ts +++ b/packages/model/src/-private.ts @@ -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'; diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index 23b3ea4eba5..adce27c87cd 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -10,6 +10,7 @@ import { isStableIdentifier, peekCache, recordIdentifierFor, + RelatedCollection as ManyArray, SOURCE, storeFor, } from '@ember-data/store/-private'; @@ -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'; @@ -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, diff --git a/packages/model/src/-private/model.type-test.ts b/packages/model/src/-private/model.type-test.ts index cd2f2a9c09b..a17658a90f9 100644 --- a/packages/model/src/-private/model.type-test.ts +++ b/packages/model/src/-private/model.type-test.ts @@ -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'; diff --git a/packages/model/src/-private/promise-many-array.ts b/packages/model/src/-private/promise-many-array.ts index 8aa0873ac0b..f83cc557414 100644 --- a/packages/model/src/-private/promise-many-array.ts +++ b/packages/model/src/-private/promise-many-array.ts @@ -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 { diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index 2b13820b149..f16c2f26912 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -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'; @@ -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'; /** diff --git a/packages/model/src/-private/type-utils.ts b/packages/model/src/-private/type-utils.ts index 4f9af2a92e0..58b8f6bd2e7 100644 --- a/packages/model/src/-private/type-utils.ts +++ b/packages/model/src/-private/type-utils.ts @@ -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'; diff --git a/packages/model/src/index.ts b/packages/model/src/index.ts index 35bc6905205..b891395ab53 100644 --- a/packages/model/src/index.ts +++ b/packages/model/src/index.ts @@ -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'; diff --git a/packages/schema-record/src/-private/compute.ts b/packages/schema-record/src/-private/compute.ts index e5908c48df3..4ad4793197d 100644 --- a/packages/schema-record/src/-private/compute.ts +++ b/packages/schema-record/src/-private/compute.ts @@ -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'; @@ -13,12 +14,13 @@ 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'; @@ -26,10 +28,11 @@ 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>() + new Map>() ); export const ManagedObjectMap = getOrSetGlobal( 'ManagedObjectMap', @@ -47,7 +50,7 @@ export function computeLocal(record: typeof Proxy, 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); @@ -319,3 +322,59 @@ export function computeResource( return new ResourceRelationship(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({ + 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; +} diff --git a/packages/schema-record/src/-private/many-array-manager.ts b/packages/schema-record/src/-private/many-array-manager.ts new file mode 100644 index 00000000000..8d6af523f16 --- /dev/null +++ b/packages/schema-record/src/-private/many-array-manager.ts @@ -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(key: string, options?: BaseFinderOptions): Promise> {} +} diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index 6d5bf01f6b2..b401b03de97 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -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'; @@ -18,6 +19,7 @@ import { computeAttribute, computeDerivation, computeField, + computeHasMany, computeLocal, computeObject, computeResource, @@ -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.` @@ -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]); diff --git a/packages/store/src/-private.ts b/packages/store/src/-private.ts index 532b54b8e04..8daca216db3 100644 --- a/packages/store/src/-private.ts +++ b/packages/store/src/-private.ts @@ -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'; diff --git a/packages/store/src/-private/managers/record-array-manager.ts b/packages/store/src/-private/managers/record-array-manager.ts index a289dfa0ef0..7008f75e2f7 100644 --- a/packages/store/src/-private/managers/record-array-manager.ts +++ b/packages/store/src/-private/managers/record-array-manager.ts @@ -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'; @@ -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. diff --git a/packages/store/src/-private/record-arrays/identifier-array.ts b/packages/store/src/-private/record-arrays/identifier-array.ts index d884f92f0ec..380571c8f60 100644 --- a/packages/store/src/-private/record-arrays/identifier-array.ts +++ b/packages/store/src/-private/record-arrays/identifier-array.ts @@ -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'; @@ -125,8 +127,18 @@ function safeForEach( return instance; } -type MinimumManager = { +type PromiseTo = Omit, typeof Symbol.toStringTag>; + +type PromiseManyArray = { + length: number; + content: IdentifierArray | null; + promise: Promise> | null; +} & PromiseTo>; + +export type MinimumManager = { _syncArray: (array: IdentifierArray) => void; + mutate?(mutation: LocalRelationshipOperation): void; + reloadHasMany?(key: string, options?: BaseFinderOptions): Promise> | PromiseManyArray; }; /** @@ -432,7 +444,7 @@ export class IdentifierArray { }, getPrototypeOf() { - return IdentifierArray.prototype; + return Array.prototype as unknown as IdentifierArray; }, }) as IdentifierArray; diff --git a/packages/model/src/-private/many-array.ts b/packages/store/src/-private/record-arrays/many-array.ts similarity index 94% rename from packages/model/src/-private/many-array.ts rename to packages/store/src/-private/record-arrays/many-array.ts index 7778cc9931c..7b4393ca4da 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/store/src/-private/record-arrays/many-array.ts @@ -3,18 +3,6 @@ */ import { deprecate } from '@ember/debug'; -import type Store from '@ember-data/store'; -import type { CreateRecordProperties, NativeProxy } from '@ember-data/store/-private'; -import { - ARRAY_SIGNAL, - isStableIdentifier, - LiveArray, - MUTATE, - notifyArray, - recordIdentifierFor, - SOURCE, -} from '@ember-data/store/-private'; -import type { BaseFinderOptions, ModelSchema } from '@ember-data/store/types'; import type { Signal } from '@ember-data/tracking/-private'; import { addToTransaction } from '@ember-data/tracking/-private'; import { DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; @@ -29,16 +17,22 @@ import type { } from '@warp-drive/core-types/record'; import type { Links, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; -import type { LegacySupport } from './legacy-relationships-support'; +import type { BaseFinderOptions, ModelSchema } from '../../types'; +import { isStableIdentifier } from '../caches/identifier-cache'; +import { recordIdentifierFor } from '../caches/instance-cache'; +import type { CreateRecordProperties, Store } from '../store-service'; +import type { MinimumManager } from './identifier-array'; +import { ARRAY_SIGNAL, IdentifierArray, MUTATE, notifyArray, SOURCE } from './identifier-array'; +import type { NativeProxy } from './native-proxy-type-fix'; -type IdentifierArrayCreateOptions = ConstructorParameters[0]; +type IdentifierArrayCreateOptions = ConstructorParameters[0]; export interface ManyArrayCreateArgs { identifiers: StableRecordIdentifier>[]; type: TypeFromInstanceOrString; store: Store; allowMutation: boolean; - manager: LegacySupport; + manager: MinimumManager; identifier: StableRecordIdentifier; cache: Cache; @@ -93,7 +87,7 @@ export interface ManyArrayCreateArgs { @class ManyArray @public */ -export class RelatedCollection extends LiveArray { +export class RelatedCollection extends IdentifierArray { declare isAsync: boolean; /** The loading state of this array @@ -158,7 +152,7 @@ export class RelatedCollection extends LiveArray { declare links: Links | PaginationLinks | null; declare identifier: StableRecordIdentifier; declare cache: Cache; - declare _manager: LegacySupport; + declare _manager: MinimumManager; declare store: Store; declare key: string; declare type: ModelSchema; @@ -412,6 +406,10 @@ export class RelatedCollection extends LiveArray { @public */ reload(options?: BaseFinderOptions): Promise { + assert( + `Expected the manager for ManyArray to implement reloadHasMany`, + typeof this._manager.reloadHasMany === 'function' + ); // TODO this is odd, we don't ask the store for anything else like this? return this._manager.reloadHasMany(this.key, options) as Promise; } @@ -628,9 +626,10 @@ function mutateSortRelatedRecords( function mutate( collection: RelatedCollection, - mutation: Parameters[0], + mutation: Parameters>[0], _SIGNAL: Signal ) { + assert(`Expected the manager for ManyArray to implement mutate`, typeof collection._manager.mutate === 'function'); collection._manager.mutate(mutation); addToTransaction(_SIGNAL); } diff --git a/tests/warp-drive__schema-record/tests/reads/has-many-test.ts b/tests/warp-drive__schema-record/tests/reads/has-many-test.ts new file mode 100644 index 00000000000..6bba8736efe --- /dev/null +++ b/tests/warp-drive__schema-record/tests/reads/has-many-test.ts @@ -0,0 +1,306 @@ +import type { TestContext } from '@ember/test-helpers'; + +import { module, skip, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; +import type { Type } from '@warp-drive/core-types/symbols'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; + +type User = { + id: string | null; + $type: 'user'; + name: string; + friends: User[] | null; + [Type]: 'user'; +}; + +module('Reads | hasMany in linksMode', function (hooks) { + setupTest(hooks); + + test('we can use sync hasMany 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: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + 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'); + assert.true(record.friends instanceof Array, 'Friends is an instance of Array'); + assert.true(Array.isArray(record.friends), 'Friends is an array'); + assert.strictEqual(record.friends?.length, 2, 'friends has 2 items'); + assert.strictEqual(record.friends?.[0].id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0].$type, 'user', 'friends[0].user is accessible'); + assert.strictEqual(record.friends?.[0].name, 'Rey', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[0].friends?.[0].id, record.id, 'friends is reciprocal'); + }); + + test('we can update sync hasMany 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: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.friends?.length, 2, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Rey', 'friends[0].name is accessible'); + + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [{ type: 'user', id: '3' }], + }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.friends?.length, 1, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '3', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Jane', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.length, 2, 'friends[0].friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].id, '1', 'friends[0].friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].name, 'Chris', 'friends[0].friends[0].name is accessible'); + }); + + skip('we error for async hasMany access in linksMode because we are not implemented yet', 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: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: true, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + // NOTE: If this is included, we can assume the link is pre-fetched + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + 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'); + + // assert.expectAssertion( + // () => record.friends, + // 'Cannot fetch user.friends because the field is in linksMode but async is not yet supported' + // ); + }); +});