@ember-data/record-data
Version:
Provides the default resource cache (RecordData) implementation for ember-data
156 lines (140 loc) • 5.75 kB
text/typescript
import { assert, warn } from '@ember/debug';
import type { ExistingResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api';
import _normalizeLink from '../../normalize-link';
import type { UpdateRelationshipOperation } from '../-operations';
import { isBelongsTo, isHasMany, notifyChange } from '../-utils';
import type { Graph } from '../graph';
/*
Updates the "canonical" or "remote" state of a relationship, replacing any existing
state and blowing away any local changes (excepting new records).
*/
export default function updateRelationshipOperation(graph: Graph, op: UpdateRelationshipOperation) {
const relationship = graph.get(op.record, op.field);
assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship));
const { definition, state, identifier } = relationship;
const { isCollection } = definition;
const payload = op.value;
let hasRelationshipDataProperty: boolean = false;
let hasUpdatedLink: boolean = false;
if (payload.meta) {
relationship.meta = payload.meta;
}
if (payload.data !== undefined) {
hasRelationshipDataProperty = true;
if (isCollection) {
// TODO deprecate this case. We
// have tests saying we support it.
if (payload.data === null) {
payload.data = [];
}
assert(`Expected an array`, Array.isArray(payload.data));
const cache = graph.store.identifierCache;
// TODO may not need to cast to stable identifiers here since update likely does this too
graph.update(
{
op: 'replaceRelatedRecords',
record: identifier,
field: op.field,
value: payload.data.map((i) => cache.getOrCreateRecordIdentifier(i)),
},
true
);
} else {
// TODO may not need to cast to stable identifiers here since update likely does this too
graph.update(
{
op: 'replaceRelatedRecord',
record: identifier,
field: op.field,
value: payload.data
? graph.store.identifierCache.getOrCreateRecordIdentifier(payload.data as ExistingResourceIdentifierObject)
: null,
},
true
);
}
} else if (definition.isAsync === false && !state.hasReceivedData) {
hasRelationshipDataProperty = true;
if (isCollection) {
graph.update(
{
op: 'replaceRelatedRecords',
record: identifier,
field: op.field,
value: [],
},
true
);
} else {
graph.update(
{
op: 'replaceRelatedRecord',
record: identifier,
field: op.field,
value: null,
},
true
);
}
}
if (payload.links) {
let originalLinks = relationship.links;
relationship.links = payload.links;
if (payload.links.related) {
let relatedLink = _normalizeLink(payload.links.related);
let currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null;
let currentLinkHref = currentLink ? currentLink.href : null;
if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) {
warn(
`You pushed a record of type '${identifier.type}' with a relationship '${definition.key}' 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.`,
definition.isAsync || state.hasReceivedData,
{
id: 'ds.store.push-link-for-sync-relationship',
}
);
assert(
`You have pushed a record of type '${identifier.type}' with '${definition.key}' as a link, but the value of that link is not a string.`,
typeof relatedLink.href === 'string' || relatedLink.href === null
);
hasUpdatedLink = true;
}
}
}
/*
Data being pushed into the relationship might contain only data or links,
or a combination of both.
IF contains only data
IF contains both links and data
state.isEmpty -> true if is empty array (has-many) or is null (belongs-to)
state.hasReceivedData -> true
hasDematerializedInverse -> false
state.isStale -> false
allInverseRecordsAreLoaded -> run-check-to-determine
IF contains only links
state.isStale -> true
*/
relationship.state.hasFailedLoadAttempt = false;
if (hasRelationshipDataProperty) {
let relationshipIsEmpty = payload.data === null || (Array.isArray(payload.data) && payload.data.length === 0);
// we don't need to notify here as the update op we pushed in above will notify once
// membership is in the correct state.
relationship.state.hasReceivedData = true;
relationship.state.isStale = false;
relationship.state.hasDematerializedInverse = false;
relationship.state.isEmpty = relationshipIsEmpty;
} else if (hasUpdatedLink) {
// only notify stale if we have not previously received membership data.
// within this same transaction
// this prevents refetching when only one side of the relationship in the
// payload contains the info while the other side contains just a link
// this only works when the side with just a link is a belongsTo, as we
// don't know if a hasMany has full information or not.
// see #7049 for context.
if (isCollection || !relationship.state.hasReceivedData || relationship.transactionRef === 0) {
relationship.state.isStale = true;
notifyChange(graph, relationship.identifier, relationship.definition.key);
} else {
relationship.state.isStale = false;
}
}
}