UNPKG

@ember-data/record-data

Version:

Provides the default resource cache (RecordData) implementation for ember-data

245 lines (222 loc) 9.58 kB
import { assert, inspect, warn } from '@ember/debug'; import { LOG_GRAPH } from '@ember-data/private-build-infra/debugging'; import type { Store } from '@ember-data/store/-private'; import { recordDataFor as peekRecordData } from '@ember-data/store/-private'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { Dict } from '@ember-data/types/q/utils'; import { coerceId } from '../coerce-id'; import type BelongsToRelationship from '../relationships/state/belongs-to'; import type ManyRelationship from '../relationships/state/has-many'; import type { UpdateRelationshipOperation } from './-operations'; import type { Graph, ImplicitRelationship } from './graph'; export function getStore(wrapper: RecordDataStoreWrapper | { _store: Store }): Store { assert(`expected a private _store property`, '_store' in wrapper); return wrapper._store; } export function expandingGet<T>(cache: Dict<Dict<T>>, key1: string, key2: string): T | undefined { let mainCache = (cache[key1] = cache[key1] || Object.create(null)); return mainCache[key2]; } export function expandingSet<T>(cache: Dict<Dict<T>>, key1: string, key2: string, value: T): void { let mainCache = (cache[key1] = cache[key1] || Object.create(null)); mainCache[key2] = value; } export function assertValidRelationshipPayload(graph: Graph, op: UpdateRelationshipOperation) { const relationship = graph.get(op.record, op.field); assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); const payload = op.value; const { definition, identifier, state } = relationship; const { type } = identifier; const { field } = op; const { isAsync, kind } = definition; if (payload.links) { warn( `You pushed a record of type '${type}' with a relationship '${field}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, isAsync || !!payload.data || state.hasReceivedData, { id: 'ds.store.push-link-for-sync-relationship', } ); } else if (payload.data) { if (kind === 'belongsTo') { assert( `A ${type} record was pushed into the store with the value of ${field} being ${inspect( payload.data )}, but ${field} is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, !Array.isArray(payload.data) ); assertRelationshipData(getStore(graph.store), identifier, payload.data, definition); } else if (kind === 'hasMany') { assert( `A ${type} record was pushed into the store with the value of ${field} being '${inspect( payload.data )}', but ${field} is a hasMany relationship so the value must be an array. You should probably check your data payload or serializer.`, Array.isArray(payload.data) ); if (Array.isArray(payload.data)) { for (let i = 0; i < payload.data.length; i++) { assertRelationshipData(getStore(graph.store), identifier, payload.data[i], definition); } } } } } export function isNew(identifier: StableRecordIdentifier): boolean { if (!identifier.id) { return true; } const recordData = peekRecordData(identifier); return Boolean(recordData?.isNew(identifier)); } export function isBelongsTo( relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship ): relationship is BelongsToRelationship { return relationship.definition.kind === 'belongsTo'; } export function isImplicit( relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship ): relationship is ImplicitRelationship { return relationship.definition.isImplicit; } export function isHasMany( relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship ): relationship is ManyRelationship { return relationship.definition.kind === 'hasMany'; } export function forAllRelatedIdentifiers( rel: BelongsToRelationship | ManyRelationship | ImplicitRelationship, cb: (identifier: StableRecordIdentifier) => void ): void { if (isBelongsTo(rel)) { if (rel.remoteState) { cb(rel.remoteState); } if (rel.localState && rel.localState !== rel.remoteState) { cb(rel.localState); } } else if (isHasMany(rel)) { // ensure we don't walk anything twice if an entry is // in both localMembers and remoteMembers let seen = new Set(); for (let i = 0; i < rel.localState.length; i++) { const inverseIdentifier = rel.localState[i]; if (!seen.has(inverseIdentifier)) { seen.add(inverseIdentifier); cb(inverseIdentifier); } } for (let i = 0; i < rel.remoteState.length; i++) { const inverseIdentifier = rel.remoteState[i]; if (!seen.has(inverseIdentifier)) { seen.add(inverseIdentifier); cb(inverseIdentifier); } } } else { let seen = new Set(); rel.localMembers.forEach((inverseIdentifier) => { if (!seen.has(inverseIdentifier)) { seen.add(inverseIdentifier); cb(inverseIdentifier); } }); rel.remoteMembers.forEach((inverseIdentifier) => { if (!seen.has(inverseIdentifier)) { seen.add(inverseIdentifier); cb(inverseIdentifier); } }); } } /* Removes the given identifier from BOTH remote AND local state. This method is useful when either a deletion or a rollback on a new record needs to entirely purge itself from an inverse relationship. */ export function removeIdentifierCompletelyFromRelationship( graph: Graph, relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship, value: StableRecordIdentifier, silenceNotifications?: boolean ): void { if (isBelongsTo(relationship)) { if (relationship.remoteState === value) { relationship.remoteState = null; } if (relationship.localState === value) { relationship.localState = null; // This allows dematerialized inverses to be rematerialized // we shouldn't be notifying here though, figure out where // a notification was missed elsewhere. if (!silenceNotifications) { notifyChange(graph, relationship.identifier, relationship.definition.key); } } } else if (isHasMany(relationship)) { relationship.remoteMembers.delete(value); relationship.localMembers.delete(value); const canonicalIndex = relationship.remoteState.indexOf(value); if (canonicalIndex !== -1) { relationship.remoteState.splice(canonicalIndex, 1); } const currentIndex = relationship.localState.indexOf(value); if (currentIndex !== -1) { relationship.localState.splice(currentIndex, 1); // This allows dematerialized inverses to be rematerialized // we shouldn't be notifying here though, figure out where // a notification was missed elsewhere. if (!silenceNotifications) { notifyChange(graph, relationship.identifier, relationship.definition.key); } } } else { relationship.remoteMembers.delete(value); relationship.localMembers.delete(value); } } // TODO add silencing at the graph level export function notifyChange(graph: Graph, identifier: StableRecordIdentifier, key: string) { if (identifier === graph._removing) { if (LOG_GRAPH) { // eslint-disable-next-line no-console console.log(`Graph: ignoring relationship change for removed identifier ${String(identifier)} ${key}`); } return; } if (LOG_GRAPH) { // eslint-disable-next-line no-console console.log(`Graph: notifying relationship change for ${String(identifier)} ${key}`); } graph.store.notifyChange(identifier, 'relationships', key); } export function assertRelationshipData(store, identifier, data, meta) { assert( `A ${identifier.type} record was pushed into the store with the value of ${meta.key} being '${JSON.stringify( data )}', but ${ meta.key } is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, !Array.isArray(data) ); assert( `Encountered a relationship identifier without a type for the ${meta.kind} relationship '${meta.key}' on <${ identifier.type }:${identifier.id}>, expected a json-api identifier with type '${meta.type}' but found '${JSON.stringify( data )}'. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, data === null || (typeof data.type === 'string' && data.type.length) ); assert( `Encountered a relationship identifier without an id for the ${meta.kind} relationship '${meta.key}' on <${ identifier.type }:${identifier.id}>, expected a json-api identifier but found '${JSON.stringify( data )}'. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, data === null || !!coerceId(data.id) ); assert( `Encountered a relationship identifier with type '${data.type}' for the ${meta.kind} relationship '${meta.key}' on <${identifier.type}:${identifier.id}>, Expected a json-api identifier with type '${meta.type}'. No model was found for '${data.type}'.`, data === null || !data.type || store.getSchemaDefinitionService().doesTypeExist(data.type) ); }