@ember-data/record-data
Version:
Provides the default resource cache (RecordData) implementation for ember-data
351 lines (296 loc) • 12.7 kB
text/typescript
import { assert } from '@ember/debug';
import type { RelationshipDefinition } from '@ember-data/model/-private/relationship-meta';
import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@ember-data/private-build-infra/deprecations';
import type Store from '@ember-data/store';
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas';
import type { Dict } from '@ember-data/types/q/utils';
import { expandingGet, expandingSet, getStore } from './-utils';
import type { Graph } from './graph';
export type EdgeCache = Dict<Dict<EdgeDefinition | null>>;
export interface UpgradedMeta {
kind: 'hasMany' | 'belongsTo' | 'implicit';
key: string;
type: string;
isAsync: boolean;
isImplicit: boolean;
isCollection: boolean;
isPolymorphic: boolean;
inverseKind: 'hasMany' | 'belongsTo' | 'implicit';
inverseKey: string;
inverseType: string;
inverseIsAsync: boolean;
inverseIsImplicit: boolean;
inverseIsCollection: boolean;
inverseIsPolymorphic: boolean;
}
export interface EdgeDefinition {
lhs_key: string;
lhs_modelNames: string[];
lhs_baseModelName: string;
lhs_relationshipName: string;
lhs_definition: UpgradedMeta;
lhs_isPolymorphic: boolean;
rhs_key: string;
rhs_modelNames: string[];
rhs_baseModelName: string;
rhs_relationshipName: string;
rhs_definition: UpgradedMeta | null;
rhs_isPolymorphic: boolean;
hasInverse: boolean;
isSelfReferential: boolean;
isReflexive: boolean;
}
const BOOL_LATER = null as unknown as boolean;
const STR_LATER = '';
const IMPLICIT_KEY_RAND = Date.now();
function implicitKeyFor(type: string, key: string): string {
return `implicit-${type}:${key}${IMPLICIT_KEY_RAND}`;
}
function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) {
definition.inverseKind = inverseDefinition.kind;
definition.inverseKey = inverseDefinition.key;
definition.inverseType = inverseDefinition.type;
definition.inverseIsAsync = inverseDefinition.isAsync;
definition.inverseIsCollection = inverseDefinition.isCollection;
definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic;
definition.inverseIsImplicit = inverseDefinition.isImplicit;
}
function upgradeMeta(meta: RelationshipSchema): UpgradedMeta {
let niceMeta: UpgradedMeta = {} as UpgradedMeta;
let options = meta.options;
niceMeta.kind = meta.kind;
niceMeta.key = meta.name;
niceMeta.type = meta.type;
assert(`Expected relationship definition to specify async`, typeof options?.async === 'boolean');
niceMeta.isAsync = options.async;
niceMeta.isImplicit = false;
niceMeta.isCollection = meta.kind === 'hasMany';
niceMeta.isPolymorphic = options && !!options.polymorphic;
niceMeta.inverseKey = (options && options.inverse) || STR_LATER;
niceMeta.inverseType = STR_LATER;
niceMeta.inverseIsAsync = BOOL_LATER;
niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER;
niceMeta.inverseIsCollection = BOOL_LATER;
return niceMeta;
}
export function isLHS(info: EdgeDefinition, type: string, key: string): boolean {
let isSelfReferential = info.isSelfReferential;
let isRelationship = key === info.lhs_relationshipName;
if (isRelationship === true) {
return (
isSelfReferential === true || // itself
type === info.lhs_baseModelName || // base or non-polymorphic
// if the other side is polymorphic then we need to scan our modelNames
(info.rhs_isPolymorphic && info.lhs_modelNames.indexOf(type) !== -1) // polymorphic
);
}
return false;
}
export function isRHS(info: EdgeDefinition, type: string, key: string): boolean {
let isSelfReferential = info.isSelfReferential;
let isRelationship = key === info.rhs_relationshipName;
if (isRelationship === true) {
return (
isSelfReferential === true || // itself
type === info.rhs_baseModelName || // base or non-polymorphic
// if the other side is polymorphic then we need to scan our modelNames
(info.lhs_isPolymorphic && info.rhs_modelNames.indexOf(type) !== -1) // polymorphic
);
}
return false;
}
export function upgradeDefinition(
graph: Graph,
identifier: StableRecordIdentifier,
propertyName: string,
isImplicit: boolean = false
): EdgeDefinition | null {
const cache = graph._definitionCache;
const storeWrapper = graph.store;
const polymorphicLookup = graph._potentialPolymorphicTypes;
const { type } = identifier;
let cached = expandingGet<EdgeDefinition | null>(cache, type, propertyName);
// CASE: We have a cached resolution (null if no relationship exists)
if (cached !== undefined) {
return cached;
}
assert(
`Expected to find relationship definition in the cache for the implicit relationship ${propertyName}`,
!isImplicit
);
let relationships = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier);
assert(`Expected to have a relationship definition for ${type} but none was found.`, relationships);
let meta = relationships[propertyName];
if (!meta) {
if (polymorphicLookup[type]) {
const altTypes = Object.keys(polymorphicLookup[type] as {});
for (let i = 0; i < altTypes.length; i++) {
let cached = expandingGet<EdgeDefinition | null>(cache, altTypes[i], propertyName);
if (cached) {
expandingSet<EdgeDefinition | null>(cache, type, propertyName, cached);
return cached;
}
}
}
// CASE: We don't have a relationship at all
// we should only hit this in prod
assert(`Expected to find a relationship definition for ${type}.${propertyName} but none was found.`, meta);
cache[type]![propertyName] = null;
return null;
}
const definition = upgradeMeta(meta);
let inverseDefinition;
let inverseKey;
const inverseType = definition.type;
// CASE: Inverse is explicitly null
if (definition.inverseKey === null) {
// TODO probably dont need this assertion if polymorphic
assert(`Expected the inverse model to exist`, getStore(storeWrapper).modelFor(inverseType));
inverseDefinition = null;
} else {
inverseKey = inverseForRelationship(getStore(storeWrapper), identifier, propertyName);
// CASE: If we are polymorphic, and we declared an inverse that is non-null
// we must assume that the lack of inverseKey means that there is no
// concrete type as the baseType, so we must construct and artificial
// placeholder
if (!inverseKey && definition.isPolymorphic && definition.inverseKey) {
inverseDefinition = {
kind: 'belongsTo', // this must be updated when we find the first belongsTo or hasMany definition that matches
key: definition.inverseKey,
type: type,
isAsync: false, // this must be updated when we find the first belongsTo or hasMany definition that matches
isImplicit: false,
isCollection: false, // this must be updated when we find the first belongsTo or hasMany definition that matches
isPolymorphic: false,
isInitialized: false, // tracks whether we have seen the other side at least once
};
// CASE: Inverse resolves to null
} else if (!inverseKey) {
inverseDefinition = null;
} else {
// CASE: We have an explicit inverse or were able to resolve one
let inverseDefinitions = storeWrapper
.getSchemaDefinitionService()
.relationshipsDefinitionFor({ type: inverseType });
assert(`Expected to have a relationship definition for ${inverseType} but none was found.`, inverseDefinitions);
let meta = inverseDefinitions[inverseKey];
assert(`Expected to find a relationship definition for ${inverseType}.${inverseKey} but none was found.`, meta);
inverseDefinition = upgradeMeta(meta);
}
}
// CASE: We have no inverse
if (!inverseDefinition) {
// polish off meta
inverseKey = implicitKeyFor(type, propertyName);
inverseDefinition = {
kind: 'implicit',
key: inverseKey,
type: type,
isAsync: false,
isImplicit: true,
isCollection: true, // with implicits any number of records could point at us
isPolymorphic: false,
};
syncMeta(definition, inverseDefinition);
syncMeta(inverseDefinition, definition);
const info = {
lhs_key: `${type}:${propertyName}`,
lhs_modelNames: [type],
lhs_baseModelName: type,
lhs_relationshipName: propertyName,
lhs_definition: definition,
lhs_isPolymorphic: definition.isPolymorphic,
rhs_key: '',
rhs_modelNames: [],
rhs_baseModelName: inverseType,
rhs_relationshipName: '',
rhs_definition: inverseDefinition,
rhs_isPolymorphic: false,
hasInverse: false,
isSelfReferential: type === inverseType, // this could be wrong if we are self-referential but also polymorphic
isReflexive: false, // we can't be reflexive if we don't define an inverse
};
expandingSet<EdgeDefinition | null>(cache, inverseType, inverseKey, info);
expandingSet<EdgeDefinition | null>(cache, type, propertyName, info);
return info;
}
// CASE: We do have an inverse
const baseType = inverseDefinition.type;
// TODO we want to assert this but this breaks all of our shoddily written tests
/*
if (DEBUG) {
let inverseDoubleCheck = inverseMeta.type.inverseFor(inverseRelationshipName, store);
assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck);
}
*/
// CASE: We may have already discovered the inverse for the baseModelName
// CASE: We have already discovered the inverse
cached = expandingGet(cache, baseType, propertyName) || expandingGet(cache, inverseType, inverseKey);
if (cached) {
// TODO this assert can be removed if the above assert is enabled
assert(
`The ${inverseType}:${inverseKey} relationship declares 'inverse: null', but it was resolved as the inverse for ${type}:${propertyName}.`,
cached.hasInverse !== false
);
let isLHS = cached.lhs_baseModelName === baseType;
let modelNames = isLHS ? cached.lhs_modelNames : cached.rhs_modelNames;
// make this lookup easier in the future by caching the key
modelNames.push(type);
expandingSet<EdgeDefinition | null>(cache, type, propertyName, cached);
return cached;
}
// this is our first time so polish off the metas
syncMeta(definition, inverseDefinition);
syncMeta(inverseDefinition, definition);
const lhs_modelNames = [type];
if (type !== baseType) {
lhs_modelNames.push(baseType);
}
const isSelfReferential = type === inverseType;
const info = {
lhs_key: `${baseType}:${propertyName}`,
lhs_modelNames,
lhs_baseModelName: baseType,
lhs_relationshipName: propertyName,
lhs_definition: definition,
lhs_isPolymorphic: definition.isPolymorphic,
rhs_key: `${inverseType}:${inverseKey}`,
rhs_modelNames: [inverseType],
rhs_baseModelName: inverseType,
rhs_relationshipName: inverseKey,
rhs_definition: inverseDefinition,
rhs_isPolymorphic: inverseDefinition.isPolymorphic,
hasInverse: true,
isSelfReferential,
isReflexive: isSelfReferential && propertyName === inverseKey,
};
// Create entries for the baseModelName as well as modelName to speed up
// inverse lookups
expandingSet<EdgeDefinition | null>(cache, baseType, propertyName, info);
expandingSet<EdgeDefinition | null>(cache, type, propertyName, info);
// Greedily populate the inverse
expandingSet<EdgeDefinition | null>(cache, inverseType, inverseKey, info);
return info;
}
function metaIsRelationshipDefinition(meta: RelationshipSchema): meta is RelationshipDefinition {
return typeof (meta as RelationshipDefinition)._inverseKey === 'function';
}
function inverseForRelationship(store: Store, identifier: StableRecordIdentifier | { type: string }, key: string) {
const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key];
if (!definition) {
return null;
}
if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) {
if (metaIsRelationshipDefinition(definition)) {
const modelClass = store.modelFor(identifier.type);
return definition._inverseKey(store, modelClass);
}
}
assert(
`Expected the relationship defintion to specify the inverse type or null.`,
definition.options?.inverse === null ||
(typeof definition.options?.inverse === 'string' && definition.options.inverse.length > 0)
);
return definition.options.inverse;
}