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

types: branded transforms and improve types needed for serializers #9279

Merged
merged 4 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/core-types/src/spec/raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,31 @@ export type ResourceIdentifierObject<T extends string = string> =
| NewResourceIdentifierObject<T>;

// TODO disallow NewResource, make narrowable
export interface SingleResourceRelationship {
data?: ExistingResourceIdentifierObject | NewResourceIdentifierObject | null;
export interface SingleResourceRelationship<T = ExistingResourceIdentifierObject | NewResourceIdentifierObject> {
data?: T | null;
meta?: Meta;
links?: Links;
}

export interface CollectionResourceRelationship {
data?: Array<ExistingResourceIdentifierObject | NewResourceIdentifierObject>;
export interface CollectionResourceRelationship<T = ExistingResourceIdentifierObject | NewResourceIdentifierObject> {
data?: T[];
meta?: Meta;
links?: PaginationLinks;
}

export type ResourceRelationshipsObject<T = ExistingResourceIdentifierObject | NewResourceIdentifierObject> = Record<
string,
SingleResourceRelationship<T> | CollectionResourceRelationship<T>
>;

/**
* Contains the data for an existing resource in JSON:API format
* @internal
*/
export interface ExistingResourceObject<T extends string = string> extends ExistingResourceIdentifierObject<T> {
meta?: Meta;
attributes?: ObjectValue;
relationships?: Record<string, SingleResourceRelationship | CollectionResourceRelationship>;
relationships?: ResourceRelationshipsObject<ExistingResourceIdentifierObject>;
links?: Links;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,76 @@
import { assert } from '@ember/debug';

import { DEBUG } from '@ember-data/env';
import type Store from '@ember-data/store';
import type { BaseFinderOptions } from '@ember-data/store/-types/q/store';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import type { RelationshipSchema } from '@warp-drive/core-types/schema';
import type { ExistingResourceObject, JsonApiDocument } from '@warp-drive/core-types/spec/raw';

import { upgradeStore } from '../-private';
import { iterateData, payloadIsNotBlank } from './legacy-data-utils';
import type { MinimumAdapterInterface } from './minimum-adapter-interface';
import { normalizeResponseHelper } from './serializer-response';

export function _findHasMany(adapter, store, identifier, link, relationship, options) {
let promise = Promise.resolve().then(() => {
export function _findHasMany(
adapter: MinimumAdapterInterface,
store: Store,
identifier: StableRecordIdentifier,
link: string | null | { href: string },
relationship: RelationshipSchema,
options: BaseFinderOptions
) {
upgradeStore(store);
const promise = Promise.resolve().then(() => {
const snapshot = store._fetchManager.createSnapshot(identifier, options);
const useLink = !link || typeof link === 'string';
const relatedLink = useLink ? link : link.href;
assert(
`Attempted to load a hasMany relationship from a specified 'link' in the original payload, but the specified link is empty. You must provide a valid 'link' in the original payload to use 'findHasMany'`,
relatedLink
);
assert(
`Expected the adapter to implement 'findHasMany' but it does not`,
typeof adapter.findHasMany === 'function'
);
return adapter.findHasMany(store, snapshot, relatedLink, relationship);
});

promise = promise.then(
(adapterPayload) => {
assert(
`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);

const serializer = store.serializerFor(relationship.type);
let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany');
return promise.then((adapterPayload) => {
assert(
`You made a 'findHasMany' request for a ${identifier.type}'s '${
relationship.name
}' relationship, using link '${JSON.stringify(link)}' , but the adapter's response did not have any data`,
payloadIsNotBlank(adapterPayload)
);
const modelClass = store.modelFor(relationship.type);

assert(
`fetched the hasMany relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: [] }`,
'data' in payload && Array.isArray(payload.data)
);
const serializer = store.serializerFor(relationship.type);
let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany');

payload = syncRelationshipDataFromLink(store, payload, identifier, relationship);
return store._push(payload, true);
},
null,
`DS: Extract payload of '${identifier.type}' : hasMany '${relationship.type}'`
);
assert(
`fetched the hasMany relationship '${relationship.name}' for ${identifier.type}:${
identifier.id
} with link '${JSON.stringify(
link
)}', but no data member is present in the response. If no data exists, the response should set { data: [] }`,
'data' in payload && Array.isArray(payload.data)
);

return promise;
payload = syncRelationshipDataFromLink(store, payload, identifier as ResourceIdentity, relationship);
return store._push(payload, true);
}, null);
}

export function _findBelongsTo(store, identifier, link, relationship, options) {
let promise = Promise.resolve().then(() => {
export function _findBelongsTo(
store: Store,
identifier: StableRecordIdentifier,
link: string | null | { href: string },
relationship: RelationshipSchema,
options: BaseFinderOptions
) {
upgradeStore(store);
const promise = Promise.resolve().then(() => {
const adapter = store.adapterFor(identifier.type);
assert(`You tried to load a belongsTo relationship but you have no adapter (for ${identifier.type})`, adapter);
assert(
Expand All @@ -50,34 +80,35 @@ export function _findBelongsTo(store, identifier, link, relationship, options) {
const snapshot = store._fetchManager.createSnapshot(identifier, options);
const useLink = !link || typeof link === 'string';
const relatedLink = useLink ? link : link.href;
assert(
`Attempted to load a belongsTo relationship from a specified 'link' in the original payload, but the specified link is empty. You must provide a valid 'link' in the original payload to use 'findBelongsTo'`,
relatedLink
);
return adapter.findBelongsTo(store, snapshot, relatedLink, relationship);
});

promise = promise.then(
(adapterPayload) => {
const modelClass = store.modelFor(relationship.type);
const serializer = store.serializerFor(relationship.type);
let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo');

assert(
`fetched the belongsTo relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: null }`,
'data' in payload &&
(payload.data === null || (typeof payload.data === 'object' && !Array.isArray(payload.data)))
);
return promise.then((adapterPayload) => {
const modelClass = store.modelFor(relationship.type);
const serializer = store.serializerFor(relationship.type);
let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo');

if (!payload.data && !payload.links && !payload.meta) {
return null;
}
assert(
`fetched the belongsTo relationship '${relationship.name}' for ${identifier.type}:${
identifier.id
} with link '${JSON.stringify(
link
)}', but no data member is present in the response. If no data exists, the response should set { data: null }`,
'data' in payload && (payload.data === null || (typeof payload.data === 'object' && !Array.isArray(payload.data)))
);

payload = syncRelationshipDataFromLink(store, payload, identifier, relationship);
if (!payload.data && !payload.links && !payload.meta) {
return null;
}

return store._push(payload, true);
},
null,
`DS: Extract payload of ${identifier.type} : ${relationship.type}`
);
payload = syncRelationshipDataFromLink(store, payload, identifier as ResourceIdentity, relationship);

return promise;
return store._push(payload, true);
}, null);
}

// sync
Expand All @@ -86,7 +117,12 @@ export function _findBelongsTo(store, identifier, link, relationship, options) {
// assert that record.relationships[inverse] is either undefined (so we can fix it)
// or provide a data: {id, type} that matches the record that requested it
// return the relationship data for the parent
function syncRelationshipDataFromLink(store, payload, parentIdentifier, relationship) {
function syncRelationshipDataFromLink(
store: Store,
payload: JsonApiDocument,
parentIdentifier: ResourceIdentity,
relationship: RelationshipSchema
) {
// ensure the right hand side (incoming payload) points to the parent record that
// requested this relationship
const relationshipData = payload.data
Expand All @@ -97,7 +133,7 @@ function syncRelationshipDataFromLink(store, payload, parentIdentifier, relation
})
: null;

const relatedDataHash = {};
const relatedDataHash = {} as JsonApiDocument;

if ('meta' in payload) {
relatedDataHash.meta = payload.meta;
Expand Down Expand Up @@ -127,7 +163,16 @@ function syncRelationshipDataFromLink(store, payload, parentIdentifier, relation
return payload;
}

function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, parentRelationship, index) {
type ResourceIdentity = { id: string; type: string };
type RelationshipData = ResourceIdentity | ResourceIdentity[] | null;

function ensureRelationshipIsSetToParent(
payload: ExistingResourceObject,
parentIdentifier: ResourceIdentity,
store: Store,
parentRelationship: RelationshipSchema,
index: number
) {
const { id, type } = payload;

if (!payload.relationships) {
Expand All @@ -139,14 +184,14 @@ function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, paren
if (inverse) {
const { inverseKey, kind } = inverse;

const relationshipData = relationships[inverseKey] && relationships[inverseKey].data;
const relationshipData = relationships[inverseKey]?.data as RelationshipData | undefined;

if (DEBUG) {
if (
typeof relationshipData !== 'undefined' &&
!relationshipDataPointsToParent(relationshipData, parentIdentifier)
) {
const inspect = function inspect(thing) {
const inspect = function inspect(thing: unknown) {
return `'${JSON.stringify(thing)}'`;
};
const quotedType = inspect(type);
Expand All @@ -159,7 +204,8 @@ function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, paren
const got = inspect(relationshipData);
const prefix = typeof index === 'number' ? `data[${index}]` : `data`;
const path = `${prefix}.relationships.${inverseKey}.data`;
const other = relationshipData ? `<${relationshipData.type}:${relationshipData.id}>` : null;
const data = Array.isArray(relationshipData) ? relationshipData[0] : relationshipData;
const other = data ? `<${data.type}:${data.id}>` : null;
const relationshipFetched = `${expectedModel}.${parentRelationship.kind}("${parentRelationship.name}")`;
const includedRecord = `<${type}:${id}>`;
const message = [
Expand All @@ -176,12 +222,12 @@ function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, paren

if (kind !== 'hasMany' || typeof relationshipData !== 'undefined') {
relationships[inverseKey] = relationships[inverseKey] || {};
relationships[inverseKey].data = fixRelationshipData(relationshipData, kind, parentIdentifier);
relationships[inverseKey].data = fixRelationshipData(relationshipData ?? null, kind, parentIdentifier);
}
}
}

function inverseForRelationship(store, identifier, key) {
function inverseForRelationship(store: Store, identifier: { type: string; id?: string }, key: string) {
const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key];
if (!definition) {
return null;
Expand All @@ -195,7 +241,12 @@ function inverseForRelationship(store, identifier, key) {
return definition.options.inverse;
}

function getInverse(store, parentIdentifier, parentRelationship, type) {
function getInverse(
store: Store,
parentIdentifier: ResourceIdentity,
parentRelationship: RelationshipSchema,
type: string
) {
const { name: lhs_relationshipName } = parentRelationship;
const { type: parentType } = parentIdentifier;
const inverseKey = inverseForRelationship(store, { type: parentType }, lhs_relationshipName);
Expand All @@ -210,7 +261,7 @@ function getInverse(store, parentIdentifier, parentRelationship, type) {
}
}

function relationshipDataPointsToParent(relationshipData, identifier) {
function relationshipDataPointsToParent(relationshipData: RelationshipData, identifier: ResourceIdentity): boolean {
if (relationshipData === null) {
return false;
}
Expand All @@ -232,37 +283,44 @@ function relationshipDataPointsToParent(relationshipData, identifier) {
return false;
}

function fixRelationshipData(relationshipData, relationshipKind, { id, type }) {
function fixRelationshipData(
relationshipData: RelationshipData,
relationshipKind: 'hasMany' | 'belongsTo',
{ id, type }: ResourceIdentity
) {
const parentRelationshipData = {
id,
type,
};

let payload;
let payload: { type: string; id: string } | { type: string; id: string }[] | null = null;

if (relationshipKind === 'hasMany') {
payload = relationshipData || [];
const relData = (relationshipData as { type: string; id: string }[]) || [];
if (relationshipData) {
assert('expected the relationship data to be an array', Array.isArray(relationshipData));
// these arrays could be massive so this is better than filter
// Note: this is potentially problematic if type/id are not in the
// same state of normalization.
const found = relationshipData.find((v) => {
return v.type === parentRelationshipData.type && v.id === parentRelationshipData.id;
});
if (!found) {
payload.push(parentRelationshipData);
relData.push(parentRelationshipData);
}
} else {
payload.push(parentRelationshipData);
relData.push(parentRelationshipData);
}
payload = relData;
} else {
payload = relationshipData || {};
Object.assign(payload, parentRelationshipData);
const relData = (relationshipData as { type: string; id: string }) || {};
Object.assign(relData, parentRelationshipData);
payload = relData;
}

return payload;
}

function validateRelationshipEntry({ id }, { id: parentModelID }) {
return id && id.toString() === parentModelID;
function validateRelationshipEntry({ id }: ResourceIdentity, { id: parentModelID }: ResourceIdentity): boolean {
return !!id && id.toString() === parentModelID;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { AdapterPayload } from './minimum-adapter-interface';

export function iterateData<T>(data: T[] | T, fn: (o: T, index?: number) => T) {
type IteratorCB<T> = ((o: T, index: number) => T) | ((o: T) => T);

export function iterateData<T>(data: T[] | T, fn: IteratorCB<T>) {
if (Array.isArray(data)) {
return data.map(fn);
} else {
return fn(data);
return fn(data, 0);
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/model/src/-private/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ class Model extends EmberObject {
binding?: T
): void;
static eachTransformedAttribute<K extends keyof this & string>(
callback: (this: ModelSchema<this>, key: K, type: string | null) => void,
callback: (this: ModelSchema<this>, key: K, type: string) => void,
binding?: T
): void;
static determineRelationshipType(
knownSide: RelationshipSchema,
store: Store
): 'oneToOne' | 'manyToOne' | 'oneToMany' | 'manyToMany' | 'oneToNone' | 'manyToNone';

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