Skip to content

Commit

Permalink
more typing
Browse files Browse the repository at this point in the history
  • Loading branch information
runspired committed Mar 12, 2024
1 parent 90c81c7 commit b19f93f
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 151 deletions.
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
4 changes: 4 additions & 0 deletions packages/model/src/-private/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ class Model extends EmberObject {
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

0 comments on commit b19f93f

Please sign in to comment.