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: support legacy attribute behaviors in SchemaRecord #9094

Merged
merged 10 commits into from
Nov 14, 2023
23 changes: 12 additions & 11 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { CacheCapabilitiesManager as InternalCapabilitiesManager } from '@e
import type { MergeOperation } from '@ember-data/store/-types/q/cache';
import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper';
import type { AttributesHash, JsonApiError, JsonApiResource } from '@ember-data/store/-types/q/record-data-json-api';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';
import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache';
import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases';
import type { Change } from '@warp-drive/core-types/cache/change';
Expand All @@ -34,7 +35,6 @@ import type {
StructuredDocument,
StructuredErrorDocument,
} from '@warp-drive/core-types/request';
import type { AttributeSchema, RelationshipSchema } from '@warp-drive/core-types/schema';
import type {
CollectionResourceDataDocument,
ResourceDataDocument,
Expand Down Expand Up @@ -450,12 +450,11 @@ export default class JSONAPICache implements Cache {

upgradeCapabilities(this._capabilities);
const store = this._capabilities._store;
const attrs = this._capabilities.getSchemaDefinitionService().attributesDefinitionFor(identifier);
Object.keys(attrs).forEach((key) => {
const attrs = this._capabilities.schema.fields(identifier);
attrs.forEach((attr, key) => {
if (key in attributes && attributes[key] !== undefined) {
return;
}
const attr = attrs[key]!;
const defaultValue = getDefaultValue(attr, identifier, store);

if (defaultValue !== undefined) {
Expand Down Expand Up @@ -708,8 +707,7 @@ export default class JSONAPICache implements Cache {

if (options !== undefined) {
const storeWrapper = this._capabilities;
const attributeDefs = storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier);
const relationshipDefs = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier);
const fields = storeWrapper.schema.fields(identifier);
const graph = this.__graph;
const propertyNames = Object.keys(options);

Expand All @@ -721,8 +719,7 @@ export default class JSONAPICache implements Cache {
continue;
}

const fieldType: AttributeSchema | RelationshipSchema | undefined =
relationshipDefs[name] || attributeDefs[name];
const fieldType: FieldSchema | undefined = fields.get(name);
const kind = fieldType !== undefined ? ('kind' in fieldType ? fieldType.kind : 'attribute') : null;
let relationship: ResourceEdge | CollectionEdge;

Expand Down Expand Up @@ -1096,7 +1093,7 @@ export default class JSONAPICache implements Cache {
} else if (cached.remoteAttrs && attr in cached.remoteAttrs) {
return cached.remoteAttrs[attr];
} else {
const attrSchema = this._capabilities.getSchemaDefinitionService().attributesDefinitionFor(identifier)[attr];
const attrSchema = this._capabilities.schema.fields(identifier).get(attr);

upgradeCapabilities(this._capabilities);
return getDefaultValue(attrSchema, identifier, this._capabilities._store);
Expand Down Expand Up @@ -1452,7 +1449,7 @@ function getRemoteState(rel: CollectionEdge | ResourceEdge) {
}

function getDefaultValue(
schema: AttributeSchema | undefined,
schema: FieldSchema | undefined,
identifier: StableRecordIdentifier,
store: Store
): Value | undefined {
Expand All @@ -1462,6 +1459,10 @@ function getDefaultValue(
return;
}

if (schema.kind !== 'attribute' && schema.kind !== 'field') {
return;
}

// legacy support for defaultValues that are functions
if (typeof options?.defaultValue === 'function') {
// If anyone opens an issue for args not working right, we'll restore + deprecate it via a Proxy
Expand All @@ -1478,7 +1479,7 @@ function getDefaultValue(
return defaultValue as Value;

// new style transforms
} else if (schema.type) {
} else if (schema.kind !== 'attribute' && schema.type) {
const transform = (
store.schema as unknown as {
transforms?: Map<
Expand Down
26 changes: 26 additions & 0 deletions packages/model/src/-private/schema-provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getOwner } from '@ember/application';

import type Store from '@ember-data/store';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';
import type { RecordIdentifier } from '@warp-drive/core-types/identifier';
import type { AttributesSchema, RelationshipsSchema } from '@warp-drive/core-types/schema';

Expand All @@ -13,11 +14,36 @@ export class ModelSchemaProvider {
declare store: ModelStore;
declare _relationshipsDefCache: Record<string, RelationshipsSchema>;
declare _attributesDefCache: Record<string, AttributesSchema>;
declare _fieldsDefCache: Record<string, Map<string, FieldSchema>>;

constructor(store: ModelStore) {
this.store = store;
this._relationshipsDefCache = Object.create(null) as Record<string, RelationshipsSchema>;
this._attributesDefCache = Object.create(null) as Record<string, AttributesSchema>;
this._fieldsDefCache = Object.create(null) as Record<string, Map<string, FieldSchema>>;
}

fields(identifier: RecordIdentifier | { type: string }): Map<string, FieldSchema> {
const { type } = identifier;
let fieldDefs: Map<string, FieldSchema> | undefined = this._fieldsDefCache[type];

if (fieldDefs === undefined) {
fieldDefs = new Map();
this._fieldsDefCache[type] = fieldDefs;

const attributes = this.attributesDefinitionFor(identifier);
const relationships = this.relationshipsDefinitionFor(identifier);

for (const attr of Object.values(attributes)) {
fieldDefs.set(attr.name, attr);
}

for (const rel of Object.values(relationships)) {
fieldDefs.set(rel.name, rel);
}
}

return fieldDefs;
}

// Following the existing RD implementation
Expand Down
9 changes: 2 additions & 7 deletions packages/model/src/migration-support.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assert } from '@ember/debug';

import { recordIdentifierFor } from '@ember-data/store';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';

import { Errors } from './-private';
import {
Expand All @@ -19,13 +20,6 @@ import {
} from './-private/model-methods';
import RecordState from './-private/record-state';

interface FieldSchema {
type: string | null;
name: string;
kind: 'attribute' | 'resource' | 'collection' | 'derived' | 'object' | 'array' | '@id' | '@local';
options?: Record<string, unknown>;
}

type Derivation<R, T> = (record: R, options: Record<string, unknown> | null, prop: string) => T;
type SchemaService = {
registerDerivation(name: string, derivation: Derivation<unknown, unknown>): void;
Expand Down Expand Up @@ -80,6 +74,7 @@ function legacySupport(record: MinimalLegacyRecord, options: Record<string, unkn
case 'constructor':
return (state._constructor = state._constructor || {
isModel: true,
name: `Record<${recordIdentifierFor(record).type}>`,
modelName: recordIdentifierFor(record).type,
});
case 'currentState':
Expand Down
28 changes: 27 additions & 1 deletion packages/schema-record/src/-base-fields.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { assert } from '@ember/debug';

import { recordIdentifierFor } from '@ember-data/store';
import { RecordInstance } from '@ember-data/store/-types/q/record-instance';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';
import type { StableRecordIdentifier } from '@warp-drive/core-types';

import { Identifier, type SchemaRecord } from './record';
import type { Derivation, FieldSchema, SchemaService } from './schema';
import type { Derivation, SchemaService } from './schema';

const Support = new WeakMap<WeakKey, Record<string, unknown>>();

export const SchemaRecordFields: FieldSchema[] = [
{
type: '@constructor',
name: 'constructor',
kind: 'derived',
},
{
name: 'id',
kind: '@id',
Expand All @@ -19,6 +29,21 @@ export const SchemaRecordFields: FieldSchema[] = [
},
];

const _constructor: Derivation<RecordInstance, unknown> = function (record) {
let state = Support.get(record as WeakKey);
if (!state) {
state = {};
Support.set(record as WeakKey, state);
}

return (state._constructor = state._constructor || {
name: `SchemaRecord<${recordIdentifierFor(record).type}>`,
get modelName() {
throw new Error('Cannot access record.constructor.modelName on non-Legacy Schema Records.');
},
});
};

export function withFields(fields: FieldSchema[]) {
fields.push(...SchemaRecordFields);
return fields;
Expand Down Expand Up @@ -49,4 +74,5 @@ export function registerDerivations(schema: SchemaService) {
'@identity',
fromIdentity as Derivation<SchemaRecord, StableRecordIdentifier | string | null>
);
schema.registerDerivation('@constructor', _constructor);
}
58 changes: 41 additions & 17 deletions packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { assert } from '@ember/debug';

import { DEBUG } from '@ember-data/env';
import type { Future } from '@ember-data/request';
import type Store from '@ember-data/store';
import type { StoreRequestInput } from '@ember-data/store/-private/cache-handler';
import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';
import {
addToTransaction,
defineSignal,
entangleSignal,
getSignal,
peekSignal,
type Signal,
Signals,
} from '@ember-data/tracking/-private';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import type { Cache } from '@warp-drive/core-types/cache';
Expand All @@ -19,7 +23,7 @@ import { STRUCTURED } from '@warp-drive/core-types/request';
import type { Link, Links } from '@warp-drive/core-types/spec/raw';
import { RecordStore } from '@warp-drive/core-types/symbols';

import type { FieldSchema, SchemaService } from './schema';
import type { SchemaService } from './schema';

export const Destroy = Symbol('Destroy');
export const Identifier = Symbol('Identifier');
Expand All @@ -29,9 +33,9 @@ export const Checkout = Symbol('Checkout');
export const Legacy = Symbol('Legacy');

const IgnoredGlobalFields = new Set(['then', STRUCTURED]);
const RecordSymbols = new Set([Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy]);
const RecordSymbols = new Set([Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, Signals]);

function computeLocal(record: SchemaRecord, field: FieldSchema, prop: string): unknown {
function computeLocal(record: typeof Proxy<SchemaRecord>, field: FieldSchema, prop: string): unknown {
let signal = peekSignal(record, prop);

if (!signal) {
Expand All @@ -42,7 +46,7 @@ function computeLocal(record: SchemaRecord, field: FieldSchema, prop: string): u
return signal.lastValue;
}

function computeAttribute(
function computeField(
schema: SchemaService,
cache: Cache,
record: SchemaRecord,
Expand All @@ -61,6 +65,10 @@ function computeAttribute(
return transform.hydrate(rawValue, field.options ?? null, record);
}

function computeAttribute(cache: Cache, identifier: StableRecordIdentifier, prop: string): unknown {
return cache.getAttr(identifier, prop);
}

function computeDerivation(
schema: SchemaService,
record: SchemaRecord,
Expand Down Expand Up @@ -176,6 +184,7 @@ export class SchemaRecord {
declare [Identifier]: StableRecordIdentifier;
declare [Editable]: boolean;
declare [Legacy]: boolean;
declare [Signals]: Map<string, Signal>;
declare ___notifications: object;

constructor(store: Store, identifier: StableRecordIdentifier, Mode: { [Editable]: boolean; [Legacy]: boolean }) {
Expand All @@ -189,6 +198,7 @@ export class SchemaRecord {
const fields = schema.fields(identifier);

const signals: Map<string, Signal> = new Map();
this[Signals] = signals;
this.___notifications = store.notifications.subscribe(
identifier,
(_: StableRecordIdentifier, type: NotificationType, key?: string) => {
Expand Down Expand Up @@ -229,25 +239,37 @@ export class SchemaRecord {

switch (field.kind) {
case '@id':
entangleSignal(signals, this, '@identity');
entangleSignal(signals, receiver, '@identity');
return identifier.id;
case '@local':
entangleSignal(signals, this, field.name);
return computeLocal(target, field, prop as string);
case '@local': {
const lastValue = computeLocal(receiver, field, prop as string);
entangleSignal(signals, receiver, prop as string);
return lastValue;
}
case 'field':
assert(
`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`,
!target[Legacy]
);
entangleSignal(signals, receiver, field.name);
return computeField(schema, cache, target, identifier, field, prop as string);
case 'attribute':
entangleSignal(signals, this, field.name);
return computeAttribute(schema, cache, target, identifier, field, prop as string);
entangleSignal(signals, receiver, field.name);
return computeAttribute(cache, identifier, prop as string);
case 'resource':
entangleSignal(signals, this, field.name);
assert(
`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`,
!target[Legacy]
);
entangleSignal(signals, receiver, field.name);
return computeResource(store, cache, target, identifier, field, prop as string);

case 'derived':
return computeDerivation(schema, receiver as unknown as SchemaRecord, identifier, field, prop as string);
default:
throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
}
},
set(target: SchemaRecord, prop: string | number | symbol, value: unknown) {
set(target: SchemaRecord, prop: string | number | symbol, value: unknown, receiver: typeof Proxy<SchemaRecord>) {
if (!IS_EDITABLE) {
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because the record is not editable`);
}
Expand All @@ -259,15 +281,14 @@ export class SchemaRecord {

switch (field.kind) {
case '@local': {
const signal = getSignal(target, prop as string, true);
const signal = getSignal(receiver, prop as string, true);
if (signal.lastValue !== value) {
signal.lastValue = value;
addToTransaction(signal);
}

return true;
}
case 'attribute': {
case 'field': {
if (field.type === null) {
cache.setAttr(identifier, prop as string, value as Value);
return true;
Expand All @@ -282,10 +303,13 @@ export class SchemaRecord {
cache.setAttr(identifier, prop as string, rawValue);
return true;
}
case 'attribute': {
cache.setAttr(identifier, prop as string, value as Value);
return true;
}
case 'derived': {
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
}

default:
throw new Error(`Unknown field kind ${field.kind}`);
}
Expand Down
Loading
Loading