Skip to content

Commit

Permalink
Merge pull request #9094 from emberjs/schema-record/legacy-attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
gitKrystan committed Nov 14, 2023
2 parents 3b24620 + 53b2890 commit 37a420f
Show file tree
Hide file tree
Showing 35 changed files with 1,538 additions and 115 deletions.
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

0 comments on commit 37a420f

Please sign in to comment.