Skip to content

Commit

Permalink
feat: (private) implement basic field support for schema-record (#8935)
Browse files Browse the repository at this point in the history
* stash schema record work

* all the TS things

* more impl work

* make defaultValue work nice

* fixup a bunch of tests

* fix lock
  • Loading branch information
runspired authored Sep 29, 2023
1 parent bab0fc9 commit 751e16f
Show file tree
Hide file tree
Showing 37 changed files with 770 additions and 287 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ module.exports = {
'ember-data-types/q/promise-proxies.ts',
'ember-data-types/q/minimum-serializer-interface.ts',
'ember-data-types/q/minimum-adapter-interface.ts',
'ember-data-types/q/identifier.ts',
'ember-data-types/q/fetch-manager.ts',
'ember-data-types/q/ember-data-json-api.ts',
'@types/@ember/polyfills/index.d.ts',
Expand Down
2 changes: 1 addition & 1 deletion ember-data-types/q/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@module @ember-data/store
*/

import { ImmutableRequestInfo } from '@ember-data/request/-private/types';
import { ImmutableRequestInfo } from '@ember-data/request';
import {
CACHE_OWNER,
DEBUG_CLIENT_ORIGINATED,
Expand Down
13 changes: 2 additions & 11 deletions ember-data-types/q/record-data-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
export interface RelationshipSchema {
kind: 'belongsTo' | 'hasMany';
type: string; // related type
/**
* @internal
* @deprecated
*/
key: string; // TODO @runspired remove our uses
// TODO @runspired should RFC be updated to make this optional?
// TODO @runspired sohuld RFC be update to enforce async and inverse are set? else internals need to know
// that meta came from @ember-data/model vs not from @ember-data/model as defaults should switch.
Expand All @@ -29,18 +24,14 @@ export interface RelationshipSchema {
export type RelationshipsSchema = Record<string, RelationshipSchema>;

export interface AttributeSchema {
/**
* @internal
*/
name: string;

kind?: 'attribute';
kind: 'attribute';

// TODO @runspired update RFC to make options optional
options?: {
[key: string]: unknown;
};
type?: string; // TODO @runspired update RFC to make type optional
type: string | null;
}

export type AttributesSchema = Record<string, AttributeSchema>;
2 changes: 1 addition & 1 deletion packages/graph/src/-private/-edge-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export function upgradeDefinition(
// TODO we want to assert this but this breaks all of our shoddily written tests
/*
if (DEBUG) {
let inverseDoubleCheck = inverseMeta.type.inverseFor(inverseRelationshipName, store);
let inverseDoubleCheck = inverseFor(inverseRelationshipName, store);
assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck);
}
Expand Down
54 changes: 49 additions & 5 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ImplicitEdge } from '@ember-data/graph/-private/edges/implicit';
import type { ResourceEdge } from '@ember-data/graph/-private/edges/resource';
import type { Graph, GraphEdge } from '@ember-data/graph/-private/graph';
import type { StructuredDataDocument, StructuredDocument, StructuredErrorDocument } from '@ember-data/request';
import Store from '@ember-data/store';
import { StoreRequestInfo } from '@ember-data/store/-private/cache-handler';
import type { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache';
import type { ResourceBlob } from '@ember-data/types/cache/aliases';
Expand Down Expand Up @@ -426,6 +427,22 @@ export default class JSONAPICache implements Cache {
}
});
}

// @ts-expect-error we reach into private api here
const store: Store = this.__storeWrapper._store;
const attrs = this.__storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier);
Object.keys(attrs).forEach((key) => {
if (key in attributes && attributes[key] !== undefined) {
return;
}
const attr = attrs[key]!;
const defaultValue = getDefaultValue(attr, identifier, store);

if (defaultValue !== undefined) {
attributes[key] = defaultValue;
}
});

return {
type,
id,
Expand Down Expand Up @@ -689,6 +706,7 @@ export default class JSONAPICache implements Cache {
switch (kind) {
case 'attribute':
this.setAttr(identifier, name, propertyValue);
createOptions[name] = propertyValue;
break;
case 'belongsTo':
this.mutate({
Expand Down Expand Up @@ -1026,7 +1044,9 @@ export default class JSONAPICache implements Cache {
return cached.remoteAttrs[attr];
} else {
const attrSchema = this.__storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier)[attr];
return getDefaultValue(attrSchema?.options);

// @ts-expect-error we reach into private API here
return getDefaultValue(attrSchema, identifier, this.__storeWrapper._store);
}
}

Expand Down Expand Up @@ -1380,22 +1400,46 @@ function getRemoteState(rel) {
return rel.remoteState;
}

function getDefaultValue(options: { defaultValue?: unknown } | undefined) {
if (!options) {
function getDefaultValue(
schema: AttributeSchema | undefined,
identifier: StableRecordIdentifier,
store: Store
): unknown {
const options = schema?.options;

if (!schema || (!options && !schema.type)) {
return;
}
if (typeof options.defaultValue === 'function') {

// 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
// that lazily instantiates the record. We don't want to provide any args here
// because in a non @ember-data/model world they don't make sense.
return options.defaultValue();
} else {
// legacy support for defaultValues that are primitives
} else if (options && 'defaultValue' in options) {
let defaultValue = options.defaultValue;
assert(
`Non primitive defaultValues are not supported because they are shared between all instances. If you would like to use a complex object as a default value please provide a function that returns the complex object.`,
typeof defaultValue !== 'object' || defaultValue === null
);
return defaultValue;

// new style transforms
} else if (schema.type) {
const transform = (
store.schema as unknown as {
transforms?: Map<
string,
{ defaultValue(options: Record<string, unknown> | null, identifier: StableRecordIdentifier): unknown }
>;
}
).transforms?.get(schema.type);

if (transform?.defaultValue) {
return transform.defaultValue(options || null, identifier);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function _findHasMany(adapter, store, identifier, link, relationship, opt
promise = promise.then(
(adapterPayload) => {
assert(
`You made a 'findHasMany' request for a ${identifier.type}'s '${relationship.key}' relationship, using link '${link}' , but the adapter's response did not have any data`,
`You made a 'findHasMany' request for a ${identifier.type}'s '${relationship.name}' relationship, using link '${link}' , but the adapter's response did not have any data`,
payloadIsNotBlank(adapterPayload)
);
const modelClass = store.modelFor(relationship.type);
Expand Down Expand Up @@ -115,7 +115,7 @@ function syncRelationshipDataFromLink(store, payload, parentIdentifier, relation
id: parentIdentifier.id,
type: parentIdentifier.type,
relationships: {
[relationship.key]: relatedDataHash,
[relationship.name]: relatedDataHash,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ function findHasMany<T>(context: StoreRequestContext): Promise<T> {
}

// identifiers case

const fetches = new Array<globalThis.Promise<StableRecordIdentifier>>(identifiers!.length);
assert(`Expected an array of identifiers to fetch`, Array.isArray(identifiers));
const fetches = new Array<globalThis.Promise<StableRecordIdentifier>>(identifiers.length);
const manager = store._fetchManager;

for (let i = 0; i < identifiers!.length; i++) {
let identifier = identifiers![i];
for (let i = 0; i < identifiers.length; i++) {
let identifier = identifiers[i];
// TODO we probably can be lenient here and return from cache for the isNew case
assertIdentifierHasId(identifier);
fetches[i] = options.reload
Expand All @@ -183,10 +183,10 @@ function saveRecord<T>(context: StoreRequestContext): Promise<T> {
try {
let payloadCopy: unknown = payload ? JSON.parse(JSON.stringify(payload)) : payload;
// eslint-disable-next-line no-console
console.log(`EmberData | Payload - ${operation!}`, payloadCopy);
console.log(`EmberData | Payload - ${operation}`, payloadCopy);
} catch (e) {
// eslint-disable-next-line no-console
console.log(`EmberData | Payload - ${operation!}`, payload);
console.log(`EmberData | Payload - ${operation}`, payload);
}
}
let result: SingleResourceDataDocument;
Expand Down
2 changes: 2 additions & 0 deletions packages/model/src/-private/attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ function attr(type, options) {

let meta = {
type: type,
kind: 'attribute',
isAttribute: true,
options: options,
key: null,
};

return computed({
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/belongs-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function belongsTo(modelName, options) {
isRelationship: true,
options: opts,
kind: 'belongsTo',
name: 'Belongs To',
name: '<Unknown BelongsTo>',
key: null,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/has-many.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function hasMany(type, options) {
options,
isRelationship: true,
kind: 'hasMany',
name: 'Has Many',
name: '<Unknown BelongsTo>',
key: null,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/-private/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Model extends EmberObject {
static relationshipsByName: Map<string, RelationshipSchema>;
static eachAttribute<T>(callback: (this: T, key: string, attribute: AttributeSchema) => void, binding?: T): void;
static eachRelationship<T>(callback: (this: T, key: string, relationship: RelationshipSchema) => void, binding?: T): void;
static eachTransformedAttribute<T>(callback: (this: T, key: string, type?: string) => void, binding?: T): void;
static eachTransformedAttribute<T>(callback: (this: T, key: string, type: string | null) => void, binding?: T): void;

static toString(): string;
static isModel: true;
Expand Down
Loading

0 comments on commit 751e16f

Please sign in to comment.