@aws-amplify/datastore
Version:
AppSyncLocal support for aws-amplify
1,098 lines (1,097 loc) • 121 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var api_1 = require("@aws-amplify/api");
var auth_1 = require("@aws-amplify/auth");
var cache_1 = require("@aws-amplify/cache");
var core_1 = require("@aws-amplify/core");
var immer_1 = require("immer");
var uuid_1 = require("uuid");
var zen_observable_ts_1 = tslib_1.__importDefault(require("zen-observable-ts"));
var authModeStrategies_1 = require("../authModeStrategies");
var predicates_1 = require("../predicates");
var storage_1 = require("../storage/storage");
var relationship_1 = require("../storage/relationship");
var sync_1 = require("../sync");
var types_1 = require("../types");
var util_1 = require("../util");
var next_1 = require("../predicates/next");
var utils_1 = require("../sync/utils");
immer_1.setAutoFreeze(true);
immer_1.enablePatches();
var logger = new core_1.ConsoleLogger('DataStore');
var ulid = util_1.monotonicUlidFactory(Date.now());
var isNode = core_1.browserOrNode().isNode;
var SETTING_SCHEMA_VERSION = 'schemaVersion';
var schema;
var modelNamespaceMap = new WeakMap();
/**
* Stores data for crafting the correct update mutation input for a model.
*
* - `Patch[]` - array of changed fields and metadata.
* - `PersistentModel` - the source model, used for diffing object-type fields.
*/
var modelPatchesMap = new WeakMap();
var getModelDefinition = function (modelConstructor) {
var namespace = modelNamespaceMap.get(modelConstructor);
var definition = namespace
? schema.namespaces[namespace].models[modelConstructor.name]
: undefined;
return definition;
};
/**
* Determines whether the given object is a Model Constructor that DataStore can
* safely use to construct objects and discover related metadata.
*
* @param obj The object to test.
*/
var isValidModelConstructor = function (obj) {
return util_1.isModelConstructor(obj) && modelNamespaceMap.has(obj);
};
var namespaceResolver = function (modelConstructor) {
var resolver = modelNamespaceMap.get(modelConstructor);
if (!resolver) {
throw new Error("Namespace Resolver for '" + modelConstructor.name + "' not found! This is probably a bug in '@amplify-js/datastore'.");
}
return resolver;
};
/**
* Creates a predicate without any conditions that can be passed to customer
* code to have conditions added to it.
*
* For example, in this query:
*
* ```ts
* await DataStore.query(
* Model,
* item => item.field.eq('value')
* );
* ```
*
* `buildSeedPredicate(Model)` is used to create `item`, which is passed to the
* predicate function, which in turn uses that "seed" predicate (`item`) to build
* a predicate tree.
*
* @param modelConstructor The model the predicate will query.
*/
var buildSeedPredicate = function (modelConstructor) {
if (!modelConstructor)
throw new Error('Missing modelConstructor');
var modelSchema = getModelDefinition(modelConstructor);
if (!modelSchema)
throw new Error('Missing modelSchema');
var pks = util_1.extractPrimaryKeyFieldNames(modelSchema);
if (!pks)
throw new Error('Could not determine PK');
return next_1.recursivePredicateFor({
builder: modelConstructor,
schema: modelSchema,
pkField: pks,
});
};
var userClasses;
var dataStoreClasses;
var storageClasses;
/**
* Maps a model to its related models for memoization/immutability.
*/
var modelInstanceAssociationsMap = new WeakMap();
/**
* Describes whether and to what a model is attached for lazy loading purposes.
*/
var ModelAttachment;
(function (ModelAttachment) {
/**
* Model doesn't lazy load from any data source.
*
* Related entity properties provided at instantiation are returned
* via the respective lazy interfaces when their properties are invoked.
*/
ModelAttachment["Detached"] = "Detached";
/**
* Model lazy loads from the global DataStore.
*/
ModelAttachment["DataStore"] = "DataStore";
/**
* Demonstrative. Not yet implemented.
*/
ModelAttachment["API"] = "API";
})(ModelAttachment || (ModelAttachment = {}));
/**
* Tells us which data source a model is attached to (lazy loads from).
*
* If `Deatched`, the model's lazy properties will only ever return properties
* from memory provided at construction time.
*/
var attachedModelInstances = new WeakMap();
/**
* Registers a model instance against a data source (DataStore, API, or
* Detached/None).
*
* The API option is demonstrative. Lazy loading against API is not yet
* implemented.
*
* @param result A model instance or array of instances
* @param attachment A ModelAttachment data source
* @returns passes the `result` back through after attachment
*/
function attached(result, attachment) {
if (Array.isArray(result)) {
result.map(function (record) { return attached(record, attachment); });
}
else {
result && attachedModelInstances.set(result, attachment);
}
return result;
}
exports.attached = attached;
/**
* Determines what source a model instance should lazy load from.
*
* If the instace was never explicitly registered, it is detached by default.
*
* @param instance A model instance
*/
exports.getAttachment = function (instance) {
return attachedModelInstances.has(instance)
? attachedModelInstances.get(instance)
: ModelAttachment.Detached;
};
var initSchema = function (userSchema) {
var _a;
if (schema !== undefined) {
console.warn('The schema has already been initialized');
return userClasses;
}
logger.log('validating schema', { schema: userSchema });
checkSchemaCodegenVersion(userSchema.codegenVersion);
var internalUserNamespace = tslib_1.__assign({ name: util_1.USER }, userSchema);
logger.log('DataStore', 'Init models');
userClasses = createTypeClasses(internalUserNamespace);
logger.log('DataStore', 'Models initialized');
var dataStoreNamespace = getNamespace();
var storageNamespace = storage_1.ExclusiveStorage.getNamespace();
var syncNamespace = sync_1.SyncEngine.getNamespace();
dataStoreClasses = createTypeClasses(dataStoreNamespace);
storageClasses = createTypeClasses(storageNamespace);
exports.syncClasses = createTypeClasses(syncNamespace);
schema = {
namespaces: (_a = {},
_a[dataStoreNamespace.name] = dataStoreNamespace,
_a[internalUserNamespace.name] = internalUserNamespace,
_a[storageNamespace.name] = storageNamespace,
_a[syncNamespace.name] = syncNamespace,
_a),
version: userSchema.version,
codegenVersion: userSchema.codegenVersion,
};
Object.keys(schema.namespaces).forEach(function (namespace) {
var e_1, _a;
var _b = tslib_1.__read(util_1.establishRelationAndKeys(schema.namespaces[namespace]), 2), relations = _b[0], keys = _b[1];
schema.namespaces[namespace].relationships = relations;
schema.namespaces[namespace].keys = keys;
var modelAssociations = new Map();
Object.values(schema.namespaces[namespace].models).forEach(function (model) {
var e_2, _a, e_3, _b;
var connectedModels = [];
Object.values(model.fields)
.filter(function (field) {
return field.association &&
field.association.connectionType === 'BELONGS_TO' &&
field.type.model !== model.name;
})
.forEach(function (field) {
return connectedModels.push(field.type.model);
});
modelAssociations.set(model.name, connectedModels);
// Precompute model info (such as pk fields) so that downstream schema consumers
// (such as predicate builders) don't have to reach back into "DataStore" space
// to go looking for it.
Object.values(model.fields).forEach(function (field) {
var relatedModel = userClasses[field.type.model];
if (util_1.isModelConstructor(relatedModel)) {
Object.defineProperty(field.type, 'modelConstructor', {
get: function () {
var relatedModelDefinition = getModelDefinition(relatedModel);
if (!relatedModelDefinition)
throw new Error("Could not find model definition for " + relatedModel.name);
return {
builder: relatedModel,
schema: relatedModelDefinition,
pkField: util_1.extractPrimaryKeyFieldNames(relatedModelDefinition),
};
},
});
}
});
// compatibility with legacy/pre-PK codegen for lazy loading to inject
// index fields into the model definition.
// definition.cloudFields = { ...definition.fields };
var indexes = schema.namespaces[namespace].relationships[model.name].indexes;
var indexFields = new Set();
try {
for (var indexes_1 = tslib_1.__values(indexes), indexes_1_1 = indexes_1.next(); !indexes_1_1.done; indexes_1_1 = indexes_1.next()) {
var index = indexes_1_1.value;
try {
for (var _c = (e_3 = void 0, tslib_1.__values(index[1])), _d = _c.next(); !_d.done; _d = _c.next()) {
var indexField = _d.value;
indexFields.add(indexField);
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
}
finally { if (e_3) throw e_3.error; }
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (indexes_1_1 && !indexes_1_1.done && (_a = indexes_1.return)) _a.call(indexes_1);
}
finally { if (e_2) throw e_2.error; }
}
model.allFields = tslib_1.__assign(tslib_1.__assign({}, Object.fromEntries(tslib_1.__spread(indexFields.values()).map(function (name) { return [
name,
{
name: name,
type: 'ID',
isArray: false,
},
]; }))), model.fields);
});
var result = new Map();
var count = 1000;
while (true && count > 0) {
if (modelAssociations.size === 0) {
break;
}
count--;
if (count === 0) {
throw new Error('Models are not topologically sortable. Please verify your schema.');
}
try {
for (var _c = (e_1 = void 0, tslib_1.__values(Array.from(modelAssociations.keys()))), _d = _c.next(); !_d.done; _d = _c.next()) {
var modelName = _d.value;
var parents = modelAssociations.get(modelName);
if (parents === null || parents === void 0 ? void 0 : parents.every(function (x) { return result.has(x); })) {
result.set(modelName, parents);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_1) throw e_1.error; }
}
Array.from(result.keys()).forEach(function (x) { return modelAssociations.delete(x); });
}
schema.namespaces[namespace].modelTopologicalOrdering = result;
});
return userClasses;
};
exports.initSchema = initSchema;
/**
* Throws an exception if the schema has *not* been initialized
* by `initSchema()`.
*
* **To be called before trying to access schema.**
*
* Currently this only needs to be called in `start()` and `clear()` because
* all other functions will call start first.
*/
var checkSchemaInitialized = function () {
if (schema === undefined) {
var message = 'Schema is not initialized. DataStore will not function as expected. This could happen if you have multiple versions of DataStore installed. Please see https://docs.amplify.aws/lib/troubleshooting/upgrading/q/platform/js/#check-for-duplicate-versions';
logger.error(message);
throw new Error(message);
}
};
/**
* Throws an exception if the schema is using a codegen version that is not supported.
*
* Set the supported version by setting majorVersion and minorVersion
* This functions similar to ^ version range.
* The tested codegenVersion major version must exactly match the set majorVersion
* The tested codegenVersion minor version must be gt or equal to the set minorVersion
* Example: For a min supported version of 5.4.0 set majorVersion = 5 and minorVersion = 4
*
* This regex will not work when setting a supported range with minor version
* of 2 or more digits.
* i.e. minorVersion = 10 will not work
* The regex will work for testing a codegenVersion with multi digit minor
* versions as long as the minimum minorVersion is single digit.
* i.e. codegenVersion = 5.30.1, majorVersion = 5, minorVersion = 4 PASSES
*
* @param codegenVersion schema codegenVersion
*/
var checkSchemaCodegenVersion = function (codegenVersion) {
var majorVersion = 3;
var minorVersion = 2;
var isValid = false;
try {
var versionParts = codegenVersion.split('.');
var _a = tslib_1.__read(versionParts, 4), major = _a[0], minor = _a[1], patch = _a[2], patchrevision = _a[3];
isValid = Number(major) === majorVersion && Number(minor) >= minorVersion;
}
catch (err) {
console.log("Error parsing codegen version: " + codegenVersion + "\n" + err);
}
if (!isValid) {
var message = "Models were generated with an unsupported version of codegen. Codegen artifacts are from " + (codegenVersion || 'an unknown version') + ", whereas ^" + majorVersion + "." + minorVersion + ".0 is required. " +
"Update to the latest CLI and run 'amplify codegen models'.";
logger.error(message);
throw new Error(message);
}
};
var createTypeClasses = function (namespace) {
var classes = {};
Object.entries(namespace.models).forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), modelName = _b[0], modelDefinition = _b[1];
var clazz = createModelClass(modelDefinition);
classes[modelName] = clazz;
modelNamespaceMap.set(clazz, namespace.name);
});
Object.entries(namespace.nonModels || {}).forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), typeName = _b[0], typeDefinition = _b[1];
var clazz = createNonModelClass(typeDefinition);
classes[typeName] = clazz;
});
return classes;
};
/**
* Collection of instantiated models to allow storage of metadata apart from
* the model visible to the consuming app -- in case the app doesn't have
* metadata fields (_version, _deleted, etc.) exposed on the model itself.
*/
var instancesMetadata = new WeakSet();
function modelInstanceCreator(modelConstructor, init) {
instancesMetadata.add(init);
return new modelConstructor(init);
}
var validateModelFields = function (modelDefinition) { return function (k, v) {
var fieldDefinition = modelDefinition.fields[k];
if (fieldDefinition !== undefined) {
var type_1 = fieldDefinition.type, isRequired_1 = fieldDefinition.isRequired, isArrayNullable = fieldDefinition.isArrayNullable, name_1 = fieldDefinition.name, isArray = fieldDefinition.isArray;
var timestamps = types_1.isSchemaModelWithAttributes(modelDefinition)
? util_1.getTimestampFields(modelDefinition)
: {};
var isTimestampField = !!timestamps[name_1];
if (((!isArray && isRequired_1) || (isArray && !isArrayNullable)) &&
!isTimestampField &&
(v === null || v === undefined)) {
throw new Error("Field " + name_1 + " is required");
}
if (types_1.isSchemaModelWithAttributes(modelDefinition) &&
!util_1.isIdManaged(modelDefinition)) {
var keys = util_1.extractPrimaryKeyFieldNames(modelDefinition);
if (keys.includes(k) && v === '') {
logger.error(util_1.errorMessages.idEmptyString, { k: k, value: v });
throw new Error(util_1.errorMessages.idEmptyString);
}
}
if (types_1.isGraphQLScalarType(type_1)) {
var jsType_1 = types_1.GraphQLScalarType.getJSType(type_1);
var validateScalar_1 = types_1.GraphQLScalarType.getValidationFunction(type_1);
if (type_1 === 'AWSJSON') {
if (typeof v === jsType_1) {
return;
}
if (typeof v === 'string') {
try {
JSON.parse(v);
return;
}
catch (error) {
throw new Error("Field " + name_1 + " is an invalid JSON object. " + v);
}
}
}
if (isArray) {
var errorTypeText = jsType_1;
if (!isRequired_1) {
errorTypeText = jsType_1 + " | null | undefined";
}
if (!Array.isArray(v) && !isArrayNullable) {
throw new Error("Field " + name_1 + " should be of type [" + errorTypeText + "], " + typeof v + " received. " + v);
}
if (!util_1.isNullOrUndefined(v) &&
v.some(function (e) {
return util_1.isNullOrUndefined(e) ? isRequired_1 : typeof e !== jsType_1;
})) {
var elemTypes = v
.map(function (e) { return (e === null ? 'null' : typeof e); })
.join(',');
throw new Error("All elements in the " + name_1 + " array should be of type " + errorTypeText + ", [" + elemTypes + "] received. " + v);
}
if (validateScalar_1 && !util_1.isNullOrUndefined(v)) {
var validationStatus = v.map(function (e) {
if (!util_1.isNullOrUndefined(e)) {
return validateScalar_1(e);
}
else if (util_1.isNullOrUndefined(e) && !isRequired_1) {
return true;
}
else {
return false;
}
});
if (!validationStatus.every(function (s) { return s; })) {
throw new Error("All elements in the " + name_1 + " array should be of type " + type_1 + ", validation failed for one or more elements. " + v);
}
}
}
else if (!isRequired_1 && v === undefined) {
return;
}
else if (typeof v !== jsType_1 && v !== null) {
throw new Error("Field " + name_1 + " should be of type " + jsType_1 + ", " + typeof v + " received. " + v);
}
else if (!util_1.isNullOrUndefined(v) &&
validateScalar_1 &&
!validateScalar_1(v) // TODO: why never, TS ... why ...
) {
throw new Error("Field " + name_1 + " should be of type " + type_1 + ", validation failed. " + v);
}
}
else if (types_1.isNonModelFieldType(type_1)) {
// do not check non model fields if undefined or null
if (!util_1.isNullOrUndefined(v)) {
var subNonModelDefinition_1 = schema.namespaces.user.nonModels[type_1.nonModel];
var modelValidator_1 = validateModelFields(subNonModelDefinition_1);
if (isArray) {
var errorTypeText = type_1.nonModel;
if (!isRequired_1) {
errorTypeText = type_1.nonModel + " | null | undefined";
}
if (!Array.isArray(v)) {
throw new Error("Field " + name_1 + " should be of type [" + errorTypeText + "], " + typeof v + " received. " + v);
}
v.forEach(function (item) {
if ((util_1.isNullOrUndefined(item) && isRequired_1) ||
(typeof item !== 'object' && typeof item !== 'undefined')) {
throw new Error("All elements in the " + name_1 + " array should be of type " + type_1.nonModel + ", [" + typeof item + "] received. " + item);
}
if (!util_1.isNullOrUndefined(item)) {
Object.keys(subNonModelDefinition_1.fields).forEach(function (subKey) {
modelValidator_1(subKey, item[subKey]);
});
}
});
}
else {
if (typeof v !== 'object') {
throw new Error("Field " + name_1 + " should be of type " + type_1.nonModel + ", " + typeof v + " recieved. " + v);
}
Object.keys(subNonModelDefinition_1.fields).forEach(function (subKey) {
modelValidator_1(subKey, v[subKey]);
});
}
}
}
}
}; };
var castInstanceType = function (modelDefinition, k, v) {
var _a = modelDefinition.fields[k] || {}, isArray = _a.isArray, type = _a.type;
// attempt to parse stringified JSON
if (typeof v === 'string' &&
(isArray ||
type === 'AWSJSON' ||
types_1.isNonModelFieldType(type) ||
types_1.isModelFieldType(type))) {
try {
return JSON.parse(v);
}
catch (_b) {
// if JSON is invalid, don't throw and let modelValidator handle it
}
}
// cast from numeric representation of boolean to JS boolean
if (typeof v === 'number' && type === 'Boolean') {
return Boolean(v);
}
return v;
};
/**
* Records the patches (as if against an empty object) used to initialize
* an instance of a Model. This can be used for determining which fields to
* send to the cloud durnig a CREATE mutation.
*/
var initPatches = new WeakMap();
/**
* Attempts to apply type-aware, casted field values from a given `init`
* object to the given `draft`.
*
* @param init The initialization object to extract field values from.
* @param modelDefinition The definition describing the target object shape.
* @param draft The draft to apply field values to.
*/
var initializeInstance = function (init, modelDefinition, draft) {
var modelValidator = validateModelFields(modelDefinition);
Object.entries(init).forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), k = _b[0], v = _b[1];
var parsedValue = castInstanceType(modelDefinition, k, v);
modelValidator(k, parsedValue);
draft[k] = parsedValue;
});
};
/**
* Updates a draft to standardize its customer-defined fields so that they are
* consistent with the data as it would look after having been synchronized from
* Cloud storage.
*
* The exceptions to this are:
*
* 1. Non-schema/Internal [sync] metadata fields.
* 2. Cloud-managed fields, which are `null` until set by cloud storage.
*
* This function should be expanded if/when deviations between canonical Cloud
* storage data and locally managed data are found. For now, the known areas
* that require normalization are:
*
* 1. Ensuring all non-metadata fields are *defined*. (I.e., turn `undefined` -> `null`.)
*
* @param modelDefinition Definition for the draft. Used to discover all fields.
* @param draft The instance draft to apply normalizations to.
*/
var normalize = function (modelDefinition, draft) {
var e_4, _a;
try {
for (var _b = tslib_1.__values(Object.keys(modelDefinition.fields)), _c = _b.next(); !_c.done; _c = _b.next()) {
var k = _c.value;
if (draft[k] === undefined)
draft[k] = null;
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
};
var createModelClass = function (modelDefinition) {
var e_5, _a;
var clazz = /** @class */ (function () {
function Model(init) {
// we create a base instance first so we can distinguish which fields were explicitly
// set by customer code versus those set by normalization. only those fields
// which are explicitly set by customers should be part of create mutations.
var patches = [];
var baseInstance = immer_1.produce(this, function (draft) {
initializeInstance(init, modelDefinition, draft);
// model is initialized inside a DataStore component (e.g. by Sync Engine, Storage Engine, etc.)
var isInternallyInitialized = instancesMetadata.has(init);
var modelInstanceMetadata = isInternallyInitialized
? init
: {};
var _id = modelInstanceMetadata.id;
if (util_1.isIdManaged(modelDefinition)) {
var isInternalModel = _id !== null && _id !== undefined;
var id = isInternalModel
? _id
: modelDefinition.syncable
? uuid_1.v4()
: ulid();
draft.id = id;
}
else if (util_1.isIdOptionallyManaged(modelDefinition)) {
// only auto-populate if the id was not provided
draft.id = draft.id || uuid_1.v4();
}
if (!isInternallyInitialized) {
checkReadOnlyPropertyOnCreate(draft, modelDefinition);
}
var _version = modelInstanceMetadata._version, _lastChangedAt = modelInstanceMetadata._lastChangedAt, _deleted = modelInstanceMetadata._deleted;
if (modelDefinition.syncable) {
draft._version = _version;
draft._lastChangedAt = _lastChangedAt;
draft._deleted = _deleted;
}
}, function (p) { return (patches = p); });
// now that we have a list of patches that encapsulate the explicit, customer-provided
// fields, we can normalize. patches from normalization are ignored, because the changes
// are only create to provide a consistent view of the data for fields pre/post sync
// where possible. (not all fields can be normalized pre-sync, because they're generally
// "cloud managed" fields, like createdAt and updatedAt.)
var normalized = immer_1.produce(baseInstance, function (draft) {
return normalize(modelDefinition, draft);
});
initPatches.set(normalized, patches);
return normalized;
}
Model.copyOf = function (source, fn) {
var modelConstructor = Object.getPrototypeOf(source || {}).constructor;
if (!isValidModelConstructor(modelConstructor)) {
var msg = 'The source object is not a valid model';
logger.error(msg, { source: source });
throw new Error(msg);
}
var patches = [];
var model = immer_1.produce(source, function (draft) {
fn(draft);
var keyNames = util_1.extractPrimaryKeyFieldNames(modelDefinition);
// Keys are immutable
keyNames.forEach(function (key) {
if (draft[key] !== source[key]) {
logger.warn("copyOf() does not update PK fields. The '" + key + "' update is being ignored.", { source: source });
}
draft[key] = source[key];
});
var modelValidator = validateModelFields(modelDefinition);
Object.entries(draft).forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), k = _b[0], v = _b[1];
var parsedValue = castInstanceType(modelDefinition, k, v);
modelValidator(k, parsedValue);
});
normalize(modelDefinition, draft);
}, function (p) { return (patches = p); });
var hasExistingPatches = modelPatchesMap.has(source);
if (patches.length || hasExistingPatches) {
if (hasExistingPatches) {
var _a = tslib_1.__read(modelPatchesMap.get(source), 2), existingPatches = _a[0], existingSource = _a[1];
var mergedPatches = util_1.mergePatches(existingSource, existingPatches, patches);
modelPatchesMap.set(model, [mergedPatches, existingSource]);
checkReadOnlyPropertyOnUpdate(mergedPatches, modelDefinition);
}
else {
modelPatchesMap.set(model, [patches, source]);
checkReadOnlyPropertyOnUpdate(patches, modelDefinition);
}
}
else {
// always register patches when performing a copyOf, even if the
// patches list is empty. this allows `save()` to recognize when an
// instance is the result of a `copyOf()`. without more significant
// refactoring, this is the only way for `save()` to know which
// diffs (patches) are relevant for `storage` to use in building
// the list of "changed" fields for mutations.
modelPatchesMap.set(model, [[], source]);
}
return attached(model, ModelAttachment.DataStore);
};
// "private" method (that's hidden via `Setting`) for `withSSRContext` to use
// to gain access to `modelInstanceCreator` and `clazz` for persisting IDs from server to client.
Model.fromJSON = function (json) {
var _this = this;
if (Array.isArray(json)) {
return json.map(function (init) { return _this.fromJSON(init); });
}
var instance = modelInstanceCreator(clazz, json);
var modelValidator = validateModelFields(modelDefinition);
Object.entries(instance).forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), k = _b[0], v = _b[1];
modelValidator(k, v);
});
return attached(instance, ModelAttachment.DataStore);
};
return Model;
}());
clazz[immer_1.immerable] = true;
Object.defineProperty(clazz, 'name', { value: modelDefinition.name });
// Add getters/setters for relationship fields.
// getter - for lazy loading
// setter - for FK management
var allModelRelationships = relationship_1.ModelRelationship.allFrom({
builder: clazz,
schema: modelDefinition,
pkField: util_1.extractPrimaryKeyFieldNames(modelDefinition),
});
var _loop_1 = function (relationship) {
var field = relationship.field;
Object.defineProperty(clazz.prototype, modelDefinition.fields[field].name, {
set: function (model) {
if (!(typeof model === 'object' || typeof model === 'undefined'))
return;
// if model is undefined or null, the connection should be removed
if (model) {
// Avoid validation error when processing AppSync response with nested
// selection set. Nested entitites lack version field and can not be validated
// TODO: explore a more reliable method to solve this
if (model.hasOwnProperty('_version')) {
var modelConstructor = Object.getPrototypeOf(model || {})
.constructor;
if (!isValidModelConstructor(modelConstructor)) {
var msg = "Value passed to " + modelDefinition.name + "." + field + " is not a valid instance of a model";
logger.error(msg, { model: model });
throw new Error(msg);
}
if (modelConstructor.name.toLowerCase() !==
relationship.remoteModelConstructor.name.toLowerCase()) {
var msg = "Value passed to " + modelDefinition.name + "." + field + " is not an instance of " + relationship.remoteModelConstructor.name;
logger.error(msg, { model: model });
throw new Error(msg);
}
}
}
// if the relationship can be managed automagically, set the FK's
if (relationship.isComplete) {
for (var i = 0; i < relationship.localJoinFields.length; i++) {
this[relationship.localJoinFields[i]] = model === null || model === void 0 ? void 0 : model[relationship.remoteJoinFields[i]];
}
var instanceMemos = modelInstanceAssociationsMap.has(this)
? modelInstanceAssociationsMap.get(this)
: modelInstanceAssociationsMap.set(this, {}).get(this);
instanceMemos[field] = model || undefined;
}
},
get: function () {
var _this = this;
/**
* Bucket for holding related models instances specific to `this` instance.
*/
var instanceMemos = modelInstanceAssociationsMap.has(this)
? modelInstanceAssociationsMap.get(this)
: modelInstanceAssociationsMap.set(this, {}).get(this);
// if the memos already has a result for this field, we'll use it.
// there is no "cache" invalidation of any kind; memos are permanent to
// keep an immutable perception of the instance.
if (!instanceMemos.hasOwnProperty(field)) {
// before we populate the memo, we need to know where to look for relatives.
// today, this only supports DataStore. Models aren't managed elsewhere in Amplify.
if (exports.getAttachment(this) === ModelAttachment.DataStore) {
// when we fetch the results using a query constructed under the guidance
// of the relationship metadata, we DO NOT AWAIT resolution. we want to
// drop the promise into the memo's synchronously, eliminating the chance
// for a race.
var resultPromise = instance.query(relationship.remoteModelConstructor, function (base) {
return base.and(function (q) {
return relationship.remoteJoinFields.map(function (field, index) {
// TODO: anything we can use instead of `any` here?
return q[field].eq(_this[relationship.localJoinFields[index]]);
});
});
});
// results in hand, how we return them to the caller depends on the relationship type.
if (relationship.type === 'HAS_MANY') {
// collections should support async iteration, even though we don't
// leverage it fully [yet].
instanceMemos[field] = new AsyncCollection(resultPromise);
}
else {
// non-collections should only ever return 1 value *or nothing*.
// if we have more than 1 record, something's amiss. it's not our job
// pick a result for the customer. it's our job to say "something's wrong."
instanceMemos[field] = resultPromise.then(function (rows) {
if (rows.length > 1) {
// should never happen for a HAS_ONE or BELONGS_TO.
var err = new Error("\n\t\t\t\t\t\t\t\t\tData integrity error.\n\t\t\t\t\t\t\t\t\tToo many records found for a HAS_ONE/BELONGS_TO field '" + modelDefinition.name + "." + field + "'\n\t\t\t\t\t\t\t\t");
console.error(err);
throw err;
}
else {
return rows[0];
}
});
}
}
else if (exports.getAttachment(this) === ModelAttachment.API) {
throw new Error('Lazy loading from API is not yet supported!');
}
else {
if (relationship.type === 'HAS_MANY') {
return new AsyncCollection([]);
}
else {
return Promise.resolve(undefined);
}
}
}
return instanceMemos[field];
},
});
};
try {
for (var allModelRelationships_1 = tslib_1.__values(allModelRelationships), allModelRelationships_1_1 = allModelRelationships_1.next(); !allModelRelationships_1_1.done; allModelRelationships_1_1 = allModelRelationships_1.next()) {
var relationship = allModelRelationships_1_1.value;
_loop_1(relationship);
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (allModelRelationships_1_1 && !allModelRelationships_1_1.done && (_a = allModelRelationships_1.return)) _a.call(allModelRelationships_1);
}
finally { if (e_5) throw e_5.error; }
}
return clazz;
};
/**
* An eventually loaded related model instance.
*/
var AsyncItem = /** @class */ (function (_super) {
tslib_1.__extends(AsyncItem, _super);
function AsyncItem() {
return _super !== null && _super.apply(this, arguments) || this;
}
return AsyncItem;
}(Promise));
exports.AsyncItem = AsyncItem;
/**
* A collection of related model instances.
*
* This collection can be async-iterated or turned directly into an array using `toArray()`.
*/
var AsyncCollection = /** @class */ (function () {
function AsyncCollection(values) {
this.values = values;
}
/**
* Facilitates async iteration.
*
* ```ts
* for await (const item of collection) {
* handle(item)
* }
* ```
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of
*/
AsyncCollection.prototype[Symbol.asyncIterator] = function () {
var _this = this;
var values;
var index = 0;
return {
next: function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var result;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!!values) return [3 /*break*/, 2];
return [4 /*yield*/, this.values];
case 1:
values = _a.sent();
_a.label = 2;
case 2:
if (index < values.length) {
result = {
value: values[index],
done: false,
};
index++;
return [2 /*return*/, result];
}
return [2 /*return*/, {
value: null,
done: true,
}];
}
});
}); },
};
};
/**
* Turns the collection into an array, up to the amount specified in `max` param.
*
* ```ts
* const all = await collection.toArray();
* const first100 = await collection.toArray({max: 100});
* ```
*/
AsyncCollection.prototype.toArray = function (_a) {
var _b = (_a === void 0 ? {} : _a).max, max = _b === void 0 ? Number.MAX_SAFE_INTEGER : _b;
var e_6, _c;
return tslib_1.__awaiter(this, void 0, void 0, function () {
var output, i, _d, _e, element, e_6_1;
return tslib_1.__generator(this, function (_f) {
switch (_f.label) {
case 0:
output = [];
i = 0;
_f.label = 1;
case 1:
_f.trys.push([1, 6, 7, 12]);
_d = tslib_1.__asyncValues(this);
_f.label = 2;
case 2: return [4 /*yield*/, _d.next()];
case 3:
if (!(_e = _f.sent(), !_e.done)) return [3 /*break*/, 5];
element = _e.value;
if (i < max) {
output.push(element);
i++;
}
else {
return [3 /*break*/, 5];
}
_f.label = 4;
case 4: return [3 /*break*/, 2];
case 5: return [3 /*break*/, 12];
case 6:
e_6_1 = _f.sent();
e_6 = { error: e_6_1 };
return [3 /*break*/, 12];
case 7:
_f.trys.push([7, , 10, 11]);
if (!(_e && !_e.done && (_c = _d.return))) return [3 /*break*/, 9];
return [4 /*yield*/, _c.call(_d)];
case 8:
_f.sent();
_f.label = 9;
case 9: return [3 /*break*/, 11];
case 10:
if (e_6) throw e_6.error;
return [7 /*endfinally*/];
case 11: return [7 /*endfinally*/];
case 12: return [2 /*return*/, output];
}
});
});
};
return AsyncCollection;
}());
exports.AsyncCollection = AsyncCollection;
var checkReadOnlyPropertyOnCreate = function (draft, modelDefinition) {
var modelKeys = Object.keys(draft);
var fields = modelDefinition.fields;
modelKeys.forEach(function (key) {
if (fields[key] && fields[key].isReadOnly) {
throw new Error(key + " is read-only.");
}
});
};
var checkReadOnlyPropertyOnUpdate = function (patches, modelDefinition) {
var patchArray = patches.map(function (p) { return [p.path[0], p.value]; });
var fields = modelDefinition.fields;
patchArray.forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), key = _b[0], val = _b[1];
if (!val || !fields[key])
return;
if (fields[key].isReadOnly) {
throw new Error(key + " is read-only.");
}
});
};
var createNonModelClass = function (typeDefinition) {
var clazz = /** @class */ (function () {
function Model(init) {
var instance = immer_1.produce(this, function (draft) {
initializeInstance(init, typeDefinition, draft);
});
return instance;
}
return Model;
}());
clazz[immer_1.immerable] = true;
Object.defineProperty(clazz, 'name', { value: typeDefinition.name });
util_1.registerNonModelClass(clazz);
return clazz;
};
function isQueryOne(obj) {
return typeof obj === 'string';
}
function defaultConflictHandler(conflictData) {
var localModel = conflictData.localModel, modelConstructor = conflictData.modelConstructor, remoteModel = conflictData.remoteModel;
var _version = remoteModel._version;
return modelInstanceCreator(modelConstructor, tslib_1.__assign(tslib_1.__assign({}, localModel), { _version: _version }));
}
function defaultErrorHandler(error) {
logger.warn(error);
}
function getModelConstructorByModelName(namespaceName, modelName) {
var result;
switch (namespaceName) {
case util_1.DATASTORE:
result = dataStoreClasses[modelName];
break;
case util_1.USER:
result = userClasses[modelName];
break;
case util_1.SYNC:
result = exports.syncClasses[modelName];
break;
case util_1.STORAGE:
result = storageClasses[modelName];
break;
default:
throw new Error("Invalid namespace: " + namespaceName);
}
if (isValidModelConstructor(result)) {
return result;
}
else {
var msg = "Model name is not valid for namespace. modelName: " + modelName + ", namespace: " + namespaceName;
logger.error(msg);
throw new Error(msg);
}
}
/**
* Queries the DataStore metadata tables to see if they are the expected
* version. If not, clobbers the whole DB. If so, leaves them alone.
* Otherwise, simply writes the schema version.
*
* SIDE EFFECT:
* 1. Creates a transaction
* 1. Updates data.
*
* @param storage Storage adapter containing the metadata.
* @param version The expected schema version.
*/
function checkSchemaVersion(storage, version) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var Setting, modelDefinition;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
Setting = dataStoreClasses.Setting;
modelDefinition = schema.namespaces[util_1.DATASTORE].models.Setting;
return [4 /*yield*/, storage.runExclusive(function (s) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var _a, schemaVersionSetting, storedValue;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, s.query(Setting, predicates_1.ModelPredicateCreator.createFromAST(modelDefinition, {
and: { key: { eq: SETTING_SCHEMA_VERSION } },
}), { page: 0, limit: 1 })];
case 1:
_a = tslib_1.__read.apply(void 0, [_b.sent(), 1]), schemaVersionSetting = _a[0];
if (!(schemaVersionSetting !== undefined &&
schemaVersionSetting.value !== undefined)) return [3 /*break*/, 4];
storedValue = JSON.parse(schemaVersionSetting.value);
if (!(storedValue !== version)) return [3 /*break*/, 3];
return [4 /*yield*/, s.clear(false)];
case 2:
_b.sent();
_b.label = 3;
case 3: return [3 /*break*/, 6];
case 4: return [4 /*yield*/, s.save(modelInstanceCreator(Setting, {
key: SETTING_SCHEMA_VERSION,
value: JSON.stringify(version),
}))];
case 5:
_b.sent();
_b.label = 6;
case 6: return [2 /*return*/];
}
});
}); })];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
var syncSubscription;
function getNamespace() {
var namespace = {
name: util_1.DATASTORE,
relationships: {},
enums: {},
nonModels: {},
models: {
Setting: {
name: 'Setting',
pluralName: 'Settings',
syncable: false,
fields: {
id: {
name: 'id',
type: 'ID',
isRequired: true,