ember-m3
Version:
Alternative to @ember-data/model in which attributes and relationships are derived from API Payloads
319 lines (290 loc) • 10.6 kB
JavaScript
import { dasherize } from '@ember/string';
import { recordDataFor } from './-private';
import { EmbeddedMegamorphicModel, EmbeddedSnapshot } from './model';
import { A } from '@ember/array';
import ManagedArray from './managed-array';
import { schemaTypesInfo, NESTED, REFERENCE, MANAGED_ARRAY } from './utils/schema-types-info';
import {
computeAttributeReference,
computeNestedModel,
computeAttribute,
resolveReferencesWithRecords,
getOrCreateRecordFromRecordData,
resolveReferencesWithInternalModels,
} from './utils/resolve';
import { CUSTOM_MODEL_CLASS } from 'ember-m3/-infra/features';
let EmbeddedInternalModel;
if (!CUSTOM_MODEL_CLASS) {
// TODO: shouldn't need this anymore; this level of indirection for nested recordData isn't useful
EmbeddedInternalModel = class EmbeddedInternalModel {
constructor({ id, modelName, parentInternalModel, parentKey, parentIdx }) {
if (CUSTOM_MODEL_CLASS) {
//asert we dont need this class anymore
return;
}
this.id = id;
this.modelName = modelName;
let recordData = recordDataFor(parentInternalModel)._getChildRecordData(
parentKey,
parentIdx,
modelName,
id,
this
);
this._recordData = recordData;
this.parentInternalModel = parentInternalModel;
this.record = null;
}
getRecord() {
return this.record;
}
createSnapshot() {
return new EmbeddedSnapshot(this.record);
}
changedAttributes() {
return this._recordData.changedAttributes();
}
};
}
// takes in a single computedValue returned by schema hooks and resolves it as either
// a reference or a nestedModel
function resolveSingleValue(computedValue, key, store, record, recordData, parentIdx, schemaType) {
// we received a resolved record and need to transfer it to the new record data
if (computedValue instanceof EmbeddedMegamorphicModel) {
// transfer ownership to the new RecordData
recordData._setChildRecordData(key, parentIdx, recordDataFor(computedValue));
return computedValue;
}
if (schemaType === REFERENCE) {
let reference = computedValue;
let { id } = reference;
if (reference.type === null) {
// for schemas with a global id-space but multiple types, schemas may
// report a type of null
if (CUSTOM_MODEL_CLASS) {
let rd = store._globalM3RecordDataCache[reference.id];
return rd ? getOrCreateRecordFromRecordData(rd, store) : null;
} else {
let internalModel = store._globalM3Cache[id];
return internalModel ? internalModel.getRecord() : null;
}
} else {
// respect the user schema's type if provided
return id !== null && id !== undefined
? store.peekRecord(reference.type, reference.id)
: null;
}
} else if (schemaType === NESTED) {
return createNestedModel(store, record, recordData, key, computedValue, parentIdx);
} else {
return computedValue;
}
}
export function resolveRecordArray(store, record, key, references) {
let recordArrayManager = store._recordArrayManager;
let array = ManagedArray.create({
modelName: '-ember-m3',
store: store,
manager: recordArrayManager,
_isAllReference: true,
key,
record,
});
if (CUSTOM_MODEL_CLASS) {
let records = resolveReferencesWithRecords(store, references);
array._setObjects(records, false);
} else {
let internalModels = resolveReferencesWithInternalModels(store, references);
array._setInternalModels(internalModels, false);
}
return array;
}
/**
* There are two different type of values we have to worry about:
* 1. References
* 2. Nested Models
*
* Here is a mapping of input -> output:
* 1. Single reference -> resolved reference
* 2. Array of references -> RecordArray of resolved references
* 3. Single nested model -> EmbeddedMegaMorphicModel
* 4. Array of nested models -> array of EmbeddedMegaMorphicModel
*/
export function resolveValue(key, value, modelName, store, schema, record, parentIdx) {
const recordData = recordDataFor(record);
const schemaInterface = recordData.schemaInterface;
let computedValue;
if (schema.useComputeAttribute()) {
computedValue = computeAttribute(key, value, modelName, schemaInterface, schema);
} else {
// TODO remove this if branch once we remove support for old compute hooks
// We invoke the old hooks and mark the results with the new apis
let computedReference = computeAttributeReference(
key,
value,
modelName,
schemaInterface,
schema
);
// First check to see if given value is either a reference or an array of references
if (computedReference) {
if (Array.isArray(computedReference)) {
computedReference.forEach((v) => schemaInterface.reference(v));
computedValue = schemaInterface.managedArray(computedReference);
} else {
computedValue = schemaInterface.reference(computedReference);
}
} else {
let computedNested = computeNestedModel(key, value, modelName, schemaInterface, schema);
computedValue = computedNested;
if (Array.isArray(computedNested)) {
computedNested.forEach((v) => schemaInterface.nested(v));
computedValue = schemaInterface.managedArray(computedNested);
} else if (computedNested !== null && typeof computedNested === 'object') {
schemaInterface.nested(computedNested);
// If computeNestedModel returned null, we used to iterate the value array manually
// and process each element individually
} else if (Array.isArray(value)) {
let content = value.map((v, i) =>
transferOrResolveValue(store, schema, record, recordData, modelName, key, v, i)
);
let array = resolveManagedArray(content, key, value, modelName, store, schema, record);
if (!CUSTOM_MODEL_CLASS) {
array._setInternalModels(
content.map((c) => c._internalModel || c),
false
);
}
return array;
}
}
}
let valueType = schemaTypesInfo.get(computedValue);
if (valueType === REFERENCE || valueType === NESTED) {
return resolveSingleValue(computedValue, key, store, record, recordData, parentIdx, valueType);
} else if (valueType === MANAGED_ARRAY) {
if (schemaTypesInfo.get(computedValue[0]) === REFERENCE) {
return resolveRecordArray(store, record, key, computedValue);
} else {
let content = computedValue.map((v, i) =>
resolveSingleValue(v, key, store, record, recordData, i, schemaTypesInfo.get(v))
);
let array = resolveManagedArray(content, key, value, modelName, store, schema, record);
if (!CUSTOM_MODEL_CLASS) {
array._setInternalModels(
content.map((c, i) => {
return schemaTypesInfo.get(computedValue[i]) === REFERENCE ? c._internalModel : c;
}),
false
);
}
return array;
}
} else if (Array.isArray(computedValue)) {
return computedValue.map((v, i) =>
resolveSingleValue(v, key, store, record, recordData, i, schemaTypesInfo.get(v))
);
} else if (computedValue) {
return computedValue;
} else {
return value;
}
}
function resolveManagedArray(content, key, value, modelName, store, schema, record) {
if (CUSTOM_MODEL_CLASS) {
return ManagedArray.create({
_objects: A(content),
key,
_value: value,
modelName,
store,
schema,
model: record,
record,
});
} else {
let array = ManagedArray.create({
key,
_value: value,
modelName,
store,
schema,
model: record,
record,
});
return array;
}
}
function transferOrResolveValue(store, schema, record, recordData, modelName, key, value, index) {
if (value instanceof EmbeddedMegamorphicModel) {
// transfer ownership to the new RecordData
recordData._setChildRecordData(key, index, recordDataFor(value));
return value;
}
return resolveValue(key, value, modelName, store, schema, record, index);
}
function createNestedModel(store, record, recordData, key, nestedValue, parentIdx = null) {
if (parentIdx !== null && nestedValue instanceof EmbeddedMegamorphicModel) {
recordData._setChildRecordData(key, parentIdx, recordDataFor(nestedValue));
return nestedValue;
}
let modelName, nestedRecordData, internalModel;
if (CUSTOM_MODEL_CLASS) {
// TODO
// for backwards compat we will still need to dasherize,
// but it would be good to confirm that this is no longer a requirement
modelName = nestedValue.type ? dasherize(nestedValue.type) : null;
nestedRecordData = recordData._getChildRecordData(key, parentIdx, modelName, nestedValue.id);
} else {
internalModel = new EmbeddedInternalModel({
// nested models with ids is pretty misleading; all they really ought to need is type
id: nestedValue.id,
// maintain consistency with internalmodel.modelName, which is normalized
// internally within ember-data
modelName: nestedValue.type ? dasherize(nestedValue.type) : null,
parentInternalModel: record._internalModel,
parentKey: key,
parentIdx,
});
}
let nestedModel;
if (CUSTOM_MODEL_CLASS) {
nestedModel = EmbeddedMegamorphicModel.create({
store,
_parentModel: record,
_topModel: record._topModel,
_recordData: nestedRecordData,
});
} else {
nestedModel = EmbeddedMegamorphicModel.create({
store,
_parentModel: record,
_topModel: record._topModel,
_internalModel: internalModel,
});
internalModel.record = nestedModel;
nestedRecordData = recordDataFor(internalModel);
}
// Detect whether the nested model itself is replaced or new, e.g. from a user having done
// `model.set('nestedModel', { ...newAttributes });`
let createDirtyNestedModel =
recordData.__attributes && Object.keys(recordData.__attributes).indexOf(key) >= 0;
// Detect whether creating model from server payload
createDirtyNestedModel =
createDirtyNestedModel && recordData.getServerAttr && !recordData.getServerAttr(key);
if (createDirtyNestedModel) {
Object.keys(nestedValue.attributes).forEach((key) => {
nestedRecordData.setAttr(key, nestedValue.attributes[key], true);
});
} else {
nestedRecordData.pushData(
{
attributes: nestedValue.attributes,
},
false,
false,
true
);
}
return nestedModel;
}