UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

1 lines • 150 kB
{"version":3,"file":"datastore.mjs","sources":["../../../src/datastore/datastore.ts"],"sourcesContent":["/* eslint-disable no-console */\n// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n// SPDX-License-Identifier: Apache-2.0\nimport { InternalAPI } from '@aws-amplify/api/internals';\nimport { Amplify, Cache, ConsoleLogger, Hub } from '@aws-amplify/core';\nimport { enablePatches, immerable, produce, setAutoFreeze, } from 'immer';\nimport { BackgroundProcessManager, amplifyUuid, } from '@aws-amplify/core/internals/utils';\nimport { Observable, filter } from 'rxjs';\nimport { defaultAuthStrategy, multiAuthStrategy } from '../authModeStrategies';\nimport { ModelPredicateCreator, ModelSortPredicateCreator, isPredicatesAll, } from '../predicates';\nimport { ExclusiveStorage as Storage } from '../storage/storage';\nimport { ModelRelationship } from '../storage/relationship';\nimport { ControlMessage, SyncEngine } from '../sync';\nimport { AuthModeStrategyType, GraphQLScalarType, isGraphQLScalarType, isIdentifierObject, isModelFieldType, isNonModelFieldType, isSchemaModelWithAttributes, } from '../types';\nimport { DATASTORE, DeferredCallbackResolver, STORAGE, SYNC, USER, errorMessages, establishRelationAndKeys, extractPrimaryKeyFieldNames, extractPrimaryKeysAndValues, getTimestampFields, inMemoryPagination, isIdManaged, isIdOptionallyManaged, isModelConstructor, isNullOrUndefined, mergePatches, monotonicUlidFactory, registerNonModelClass, sortCompareFunction, } from '../util';\nimport { internals, predicateFor, recursivePredicateFor, } from '../predicates/next';\nimport { getIdentifierValue } from '../sync/utils';\nimport { isNode } from './utils';\nsetAutoFreeze(true);\nenablePatches();\nconst logger = new ConsoleLogger('DataStore');\nconst ulid = monotonicUlidFactory(Date.now());\nconst SETTING_SCHEMA_VERSION = 'schemaVersion';\nlet schema;\nconst modelNamespaceMap = new WeakMap();\n/**\n * Stores data for crafting the correct update mutation input for a model.\n *\n * - `Patch[]` - array of changed fields and metadata.\n * - `PersistentModel` - the source model, used for diffing object-type fields.\n */\nconst modelPatchesMap = new WeakMap();\nconst getModelDefinition = (modelConstructor) => {\n const namespace = modelNamespaceMap.get(modelConstructor);\n const definition = namespace\n ? schema.namespaces[namespace].models[modelConstructor.name]\n : undefined;\n return definition;\n};\n/**\n * Determines whether the given object is a Model Constructor that DataStore can\n * safely use to construct objects and discover related metadata.\n *\n * @param obj The object to test.\n */\nconst isValidModelConstructor = (obj) => {\n return isModelConstructor(obj) && modelNamespaceMap.has(obj);\n};\nconst namespaceResolver = modelConstructor => {\n const resolver = modelNamespaceMap.get(modelConstructor);\n if (!resolver) {\n throw new Error(`Namespace Resolver for '${modelConstructor.name}' not found! This is probably a bug in '@amplify-js/datastore'.`);\n }\n return resolver;\n};\n/**\n * Creates a predicate without any conditions that can be passed to customer\n * code to have conditions added to it.\n *\n * For example, in this query:\n *\n * ```ts\n * await DataStore.query(\n * \tModel,\n * \titem => item.field.eq('value')\n * );\n * ```\n *\n * `buildSeedPredicate(Model)` is used to create `item`, which is passed to the\n * predicate function, which in turn uses that \"seed\" predicate (`item`) to build\n * a predicate tree.\n *\n * @param modelConstructor The model the predicate will query.\n */\nconst buildSeedPredicate = (modelConstructor) => {\n if (!modelConstructor)\n throw new Error('Missing modelConstructor');\n const modelSchema = getModelDefinition(modelConstructor);\n if (!modelSchema)\n throw new Error('Missing modelSchema');\n const pks = extractPrimaryKeyFieldNames(modelSchema);\n if (!pks)\n throw new Error('Could not determine PK');\n return recursivePredicateFor({\n builder: modelConstructor,\n schema: modelSchema,\n pkField: pks,\n });\n};\n// exporting syncClasses for testing outbox.test.ts\n// TODO(eslint): refactor not to export non-constant\n// eslint-disable-next-line import/no-mutable-exports\nexport let syncClasses;\nlet userClasses;\nlet dataStoreClasses;\nlet storageClasses;\n/**\n * Maps a model to its related models for memoization/immutability.\n */\nconst modelInstanceAssociationsMap = new WeakMap();\n/**\n * Describes whether and to what a model is attached for lazy loading purposes.\n */\nvar ModelAttachment;\n(function (ModelAttachment) {\n /**\n * Model doesn't lazy load from any data source.\n *\n * Related entity properties provided at instantiation are returned\n * via the respective lazy interfaces when their properties are invoked.\n */\n ModelAttachment[\"Detached\"] = \"Detached\";\n /**\n * Model lazy loads from the global DataStore.\n */\n ModelAttachment[\"DataStore\"] = \"DataStore\";\n /**\n * Demonstrative. Not yet implemented.\n */\n ModelAttachment[\"API\"] = \"API\";\n})(ModelAttachment || (ModelAttachment = {}));\n/**\n * Tells us which data source a model is attached to (lazy loads from).\n *\n * If `Deatched`, the model's lazy properties will only ever return properties\n * from memory provided at construction time.\n */\nconst attachedModelInstances = new WeakMap();\n/**\n * Registers a model instance against a data source (DataStore, API, or\n * Detached/None).\n *\n * The API option is demonstrative. Lazy loading against API is not yet\n * implemented.\n *\n * @param result A model instance or array of instances\n * @param attachment A ModelAttachment data source\n * @returns passes the `result` back through after attachment\n */\nexport function attached(result, attachment) {\n if (Array.isArray(result)) {\n result.map(record => attached(record, attachment));\n }\n else {\n result && attachedModelInstances.set(result, attachment);\n }\n return result;\n}\n/**\n * Determines what source a model instance should lazy load from.\n *\n * If the instace was never explicitly registered, it is detached by default.\n *\n * @param instance A model instance\n */\nexport const getAttachment = (instance) => {\n return attachedModelInstances.has(instance)\n ? attachedModelInstances.get(instance)\n : ModelAttachment.Detached;\n};\nconst initSchema = (userSchema) => {\n if (schema !== undefined) {\n console.warn('The schema has already been initialized');\n return userClasses;\n }\n logger.log('validating schema', { schema: userSchema });\n checkSchemaCodegenVersion(userSchema.codegenVersion);\n const internalUserNamespace = {\n name: USER,\n ...userSchema,\n };\n logger.log('DataStore', 'Init models');\n userClasses = createTypeClasses(internalUserNamespace);\n logger.log('DataStore', 'Models initialized');\n const dataStoreNamespace = getNamespace();\n const storageNamespace = Storage.getNamespace();\n const syncNamespace = SyncEngine.getNamespace();\n dataStoreClasses = createTypeClasses(dataStoreNamespace);\n storageClasses = createTypeClasses(storageNamespace);\n syncClasses = createTypeClasses(syncNamespace);\n schema = {\n namespaces: {\n [dataStoreNamespace.name]: dataStoreNamespace,\n [internalUserNamespace.name]: internalUserNamespace,\n [storageNamespace.name]: storageNamespace,\n [syncNamespace.name]: syncNamespace,\n },\n version: userSchema.version,\n codegenVersion: userSchema.codegenVersion,\n };\n Object.keys(schema.namespaces).forEach(namespace => {\n const [relations, keys] = establishRelationAndKeys(schema.namespaces[namespace]);\n schema.namespaces[namespace].relationships = relations;\n schema.namespaces[namespace].keys = keys;\n const modelAssociations = new Map();\n Object.values(schema.namespaces[namespace].models).forEach(model => {\n const connectedModels = [];\n Object.values(model.fields)\n .filter(field => field.association &&\n field.association.connectionType === 'BELONGS_TO' &&\n field.type.model !== model.name)\n .forEach(field => connectedModels.push(field.type.model));\n modelAssociations.set(model.name, connectedModels);\n // Precompute model info (such as pk fields) so that downstream schema consumers\n // (such as predicate builders) don't have to reach back into \"DataStore\" space\n // to go looking for it.\n Object.values(model.fields).forEach(field => {\n const relatedModel = userClasses[field.type.model];\n if (isModelConstructor(relatedModel)) {\n Object.defineProperty(field.type, 'modelConstructor', {\n get: () => {\n const relatedModelDefinition = getModelDefinition(relatedModel);\n if (!relatedModelDefinition)\n throw new Error(`Could not find model definition for ${relatedModel.name}`);\n return {\n builder: relatedModel,\n schema: relatedModelDefinition,\n pkField: extractPrimaryKeyFieldNames(relatedModelDefinition),\n };\n },\n });\n }\n });\n // compatibility with legacy/pre-PK codegen for lazy loading to inject\n // index fields into the model definition.\n // definition.cloudFields = { ...definition.fields };\n const { indexes } = schema.namespaces[namespace].relationships[model.name];\n const indexFields = new Set();\n for (const index of indexes) {\n for (const indexField of index[1]) {\n indexFields.add(indexField);\n }\n }\n model.allFields = {\n ...Object.fromEntries([...indexFields.values()].map(name => [\n name,\n {\n name,\n type: 'ID',\n isArray: false,\n },\n ])),\n ...model.fields,\n };\n });\n const result = new Map();\n let count = 1000;\n // eslint-disable-next-line no-constant-binary-expression\n while (true && count > 0) {\n if (modelAssociations.size === 0) {\n break;\n }\n count--;\n if (count === 0) {\n throw new Error('Models are not topologically sortable. Please verify your schema.');\n }\n for (const modelName of Array.from(modelAssociations.keys())) {\n const parents = modelAssociations.get(modelName);\n if (parents?.every(x => result.has(x))) {\n result.set(modelName, parents);\n }\n }\n Array.from(result.keys()).forEach(x => modelAssociations.delete(x));\n }\n schema.namespaces[namespace].modelTopologicalOrdering = result;\n });\n return userClasses;\n};\n/**\n * Throws an exception if the schema has *not* been initialized\n * by `initSchema()`.\n *\n * **To be called before trying to access schema.**\n *\n * Currently this only needs to be called in `start()` and `clear()` because\n * all other functions will call start first.\n */\nconst checkSchemaInitialized = () => {\n if (schema === undefined) {\n const 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';\n logger.error(message);\n throw new Error(message);\n }\n};\n/**\n * Throws an exception if the schema is using a codegen version that is not supported.\n *\n * Set the supported version by setting majorVersion and minorVersion\n * This functions similar to ^ version range.\n * The tested codegenVersion major version must exactly match the set majorVersion\n * The tested codegenVersion minor version must be gt or equal to the set minorVersion\n * Example: For a min supported version of 5.4.0 set majorVersion = 5 and minorVersion = 4\n *\n * This regex will not work when setting a supported range with minor version\n * of 2 or more digits.\n * i.e. minorVersion = 10 will not work\n * The regex will work for testing a codegenVersion with multi digit minor\n * versions as long as the minimum minorVersion is single digit.\n * i.e. codegenVersion = 5.30.1, majorVersion = 5, minorVersion = 4 PASSES\n *\n * @param codegenVersion schema codegenVersion\n */\nconst checkSchemaCodegenVersion = (codegenVersion) => {\n const majorVersion = 3;\n const minorVersion = 2;\n let isValid = false;\n try {\n const versionParts = codegenVersion.split('.');\n const [major, minor] = versionParts;\n isValid = Number(major) === majorVersion && Number(minor) >= minorVersion;\n }\n catch (err) {\n console.log(`Error parsing codegen version: ${codegenVersion}\\n${err}`);\n }\n if (!isValid) {\n const message = `Models were generated with an unsupported version of codegen. Codegen artifacts are from ${codegenVersion || 'an unknown version'}, whereas ^${majorVersion}.${minorVersion}.0 is required. ` +\n \"Update to the latest CLI and run 'amplify codegen models'.\";\n logger.error(message);\n throw new Error(message);\n }\n};\nconst createTypeClasses = namespace => {\n const classes = {};\n Object.entries(namespace.models).forEach(([modelName, modelDefinition]) => {\n const clazz = createModelClass(modelDefinition);\n classes[modelName] = clazz;\n modelNamespaceMap.set(clazz, namespace.name);\n });\n Object.entries(namespace.nonModels || {}).forEach(([typeName, typeDefinition]) => {\n const clazz = createNonModelClass(typeDefinition);\n classes[typeName] = clazz;\n });\n return classes;\n};\n/**\n * Collection of instantiated models to allow storage of metadata apart from\n * the model visible to the consuming app -- in case the app doesn't have\n * metadata fields (_version, _deleted, etc.) exposed on the model itself.\n */\nconst instancesMetadata = new WeakSet();\nfunction modelInstanceCreator(ModelConstructor, init) {\n instancesMetadata.add(init);\n return new ModelConstructor(init);\n}\nconst validateModelFields = (modelDefinition) => (k, v) => {\n const fieldDefinition = modelDefinition.fields[k];\n if (fieldDefinition !== undefined) {\n const { type, isRequired, isArrayNullable, name, isArray } = fieldDefinition;\n const timestamps = isSchemaModelWithAttributes(modelDefinition)\n ? getTimestampFields(modelDefinition)\n : {};\n const isTimestampField = !!timestamps[name];\n if (((!isArray && isRequired) || (isArray && !isArrayNullable)) &&\n !isTimestampField &&\n (v === null || v === undefined)) {\n throw new Error(`Field ${name} is required`);\n }\n if (isSchemaModelWithAttributes(modelDefinition) &&\n !isIdManaged(modelDefinition)) {\n const keys = extractPrimaryKeyFieldNames(modelDefinition);\n if (keys.includes(k) && v === '') {\n logger.error(errorMessages.idEmptyString, { k, value: v });\n throw new Error(errorMessages.idEmptyString);\n }\n }\n if (isGraphQLScalarType(type)) {\n const jsType = GraphQLScalarType.getJSType(type);\n const validateScalar = GraphQLScalarType.getValidationFunction(type);\n if (type === 'AWSJSON') {\n if (typeof v === jsType) {\n return;\n }\n if (typeof v === 'string') {\n try {\n JSON.parse(v);\n return;\n }\n catch (error) {\n throw new Error(`Field ${name} is an invalid JSON object. ${v}`);\n }\n }\n }\n if (isArray) {\n let errorTypeText = jsType;\n if (!isRequired) {\n errorTypeText = `${jsType} | null | undefined`;\n }\n if (!Array.isArray(v) && !isArrayNullable) {\n throw new Error(`Field ${name} should be of type [${errorTypeText}], ${typeof v} received. ${v}`);\n }\n if (!isNullOrUndefined(v) &&\n v.some(e => isNullOrUndefined(e) ? isRequired : typeof e !== jsType)) {\n const elemTypes = v\n .map(e => (e === null ? 'null' : typeof e))\n .join(',');\n throw new Error(`All elements in the ${name} array should be of type ${errorTypeText}, [${elemTypes}] received. ${v}`);\n }\n if (validateScalar && !isNullOrUndefined(v)) {\n const validationStatus = v.map(e => {\n if (!isNullOrUndefined(e)) {\n return validateScalar(e);\n }\n else if (isNullOrUndefined(e) && !isRequired) {\n return true;\n }\n else {\n return false;\n }\n });\n if (!validationStatus.every(s => s)) {\n throw new Error(`All elements in the ${name} array should be of type ${type}, validation failed for one or more elements. ${v}`);\n }\n }\n }\n else if (!isRequired && v === undefined) {\n // no-op for this branch but still to filter this branch out\n }\n else if (typeof v !== jsType && v !== null) {\n throw new Error(`Field ${name} should be of type ${jsType}, ${typeof v} received. ${v}`);\n }\n else if (!isNullOrUndefined(v) &&\n validateScalar &&\n !validateScalar(v) // TODO: why never, TS ... why ...\n ) {\n throw new Error(`Field ${name} should be of type ${type}, validation failed. ${v}`);\n }\n }\n else if (isNonModelFieldType(type)) {\n // do not check non model fields if undefined or null\n if (!isNullOrUndefined(v)) {\n const subNonModelDefinition = schema.namespaces.user.nonModels[type.nonModel];\n const modelValidator = validateModelFields(subNonModelDefinition);\n if (isArray) {\n let errorTypeText = type.nonModel;\n if (!isRequired) {\n errorTypeText = `${type.nonModel} | null | undefined`;\n }\n if (!Array.isArray(v)) {\n throw new Error(`Field ${name} should be of type [${errorTypeText}], ${typeof v} received. ${v}`);\n }\n v.forEach(item => {\n if ((isNullOrUndefined(item) && isRequired) ||\n (typeof item !== 'object' && typeof item !== 'undefined')) {\n throw new Error(`All elements in the ${name} array should be of type ${type.nonModel}, [${typeof item}] received. ${item}`);\n }\n if (!isNullOrUndefined(item)) {\n Object.keys(subNonModelDefinition.fields).forEach(subKey => {\n modelValidator(subKey, item[subKey]);\n });\n }\n });\n }\n else {\n if (typeof v !== 'object') {\n throw new Error(`Field ${name} should be of type ${type.nonModel}, ${typeof v} recieved. ${v}`);\n }\n Object.keys(subNonModelDefinition.fields).forEach(subKey => {\n modelValidator(subKey, v[subKey]);\n });\n }\n }\n }\n }\n};\nconst castInstanceType = (modelDefinition, k, v) => {\n const { isArray, type } = modelDefinition.fields[k] || {};\n // attempt to parse stringified JSON\n if (typeof v === 'string' &&\n (isArray ||\n type === 'AWSJSON' ||\n isNonModelFieldType(type) ||\n isModelFieldType(type))) {\n try {\n return JSON.parse(v);\n }\n catch {\n // if JSON is invalid, don't throw and let modelValidator handle it\n }\n }\n // cast from numeric representation of boolean to JS boolean\n if (typeof v === 'number' && type === 'Boolean') {\n return Boolean(v);\n }\n return v;\n};\n/**\n * Records the patches (as if against an empty object) used to initialize\n * an instance of a Model. This can be used for determining which fields to\n * send to the cloud durnig a CREATE mutation.\n */\nconst initPatches = new WeakMap();\n/**\n * Attempts to apply type-aware, casted field values from a given `init`\n * object to the given `draft`.\n *\n * @param init The initialization object to extract field values from.\n * @param modelDefinition The definition describing the target object shape.\n * @param draft The draft to apply field values to.\n */\nconst initializeInstance = (init, modelDefinition, draft) => {\n const modelValidator = validateModelFields(modelDefinition);\n Object.entries(init).forEach(([k, v]) => {\n const parsedValue = castInstanceType(modelDefinition, k, v);\n modelValidator(k, parsedValue);\n draft[k] = parsedValue;\n });\n};\n/**\n * Updates a draft to standardize its customer-defined fields so that they are\n * consistent with the data as it would look after having been synchronized from\n * Cloud storage.\n *\n * The exceptions to this are:\n *\n * 1. Non-schema/Internal [sync] metadata fields.\n * 2. Cloud-managed fields, which are `null` until set by cloud storage.\n *\n * This function should be expanded if/when deviations between canonical Cloud\n * storage data and locally managed data are found. For now, the known areas\n * that require normalization are:\n *\n * 1. Ensuring all non-metadata fields are *defined*. (I.e., turn `undefined` -> `null`.)\n *\n * @param modelDefinition Definition for the draft. Used to discover all fields.\n * @param draft The instance draft to apply normalizations to.\n */\nconst normalize = (modelDefinition, draft) => {\n for (const k of Object.keys(modelDefinition.fields)) {\n if (draft[k] === undefined)\n draft[k] = null;\n }\n};\nconst createModelClass = (modelDefinition) => {\n const clazz = class Model {\n constructor(init) {\n // we create a base instance first so we can distinguish which fields were explicitly\n // set by customer code versus those set by normalization. only those fields\n // which are explicitly set by customers should be part of create mutations.\n let patches = [];\n const baseInstance = produce(this, (draft) => {\n initializeInstance(init, modelDefinition, draft);\n // model is initialized inside a DataStore component (e.g. by Sync Engine, Storage Engine, etc.)\n const isInternallyInitialized = instancesMetadata.has(init);\n const modelInstanceMetadata = isInternallyInitialized\n ? init\n : {};\n const { id: _id } = modelInstanceMetadata;\n if (isIdManaged(modelDefinition)) {\n const isInternalModel = _id !== null && _id !== undefined;\n const id = isInternalModel\n ? _id\n : modelDefinition.syncable\n ? amplifyUuid()\n : ulid();\n draft.id = id;\n }\n else if (isIdOptionallyManaged(modelDefinition)) {\n // only auto-populate if the id was not provided\n draft.id =\n draft.id || amplifyUuid();\n }\n if (!isInternallyInitialized) {\n checkReadOnlyPropertyOnCreate(draft, modelDefinition);\n }\n const { _version, _lastChangedAt, _deleted } = modelInstanceMetadata;\n if (modelDefinition.syncable) {\n draft._version = _version;\n draft._lastChangedAt = _lastChangedAt;\n draft._deleted = _deleted;\n }\n }, p => (patches = p));\n // now that we have a list of patches that encapsulate the explicit, customer-provided\n // fields, we can normalize. patches from normalization are ignored, because the changes\n // are only create to provide a consistent view of the data for fields pre/post sync\n // where possible. (not all fields can be normalized pre-sync, because they're generally\n // \"cloud managed\" fields, like createdAt and updatedAt.)\n const normalized = produce(baseInstance, (draft) => {\n normalize(modelDefinition, draft);\n });\n initPatches.set(normalized, patches);\n return normalized;\n }\n static copyOf(source, fn) {\n const modelConstructor = Object.getPrototypeOf(source || {}).constructor;\n if (!isValidModelConstructor(modelConstructor)) {\n const msg = 'The source object is not a valid model';\n logger.error(msg, { source });\n throw new Error(msg);\n }\n let patches = [];\n const model = produce(source, draft => {\n fn(draft);\n const keyNames = extractPrimaryKeyFieldNames(modelDefinition);\n // Keys are immutable\n keyNames.forEach(key => {\n if (draft[key] !== source[key]) {\n logger.warn(`copyOf() does not update PK fields. The '${key}' update is being ignored.`, { source });\n }\n draft[key] = source[key];\n });\n const modelValidator = validateModelFields(modelDefinition);\n Object.entries(draft).forEach(([k, v]) => {\n const parsedValue = castInstanceType(modelDefinition, k, v);\n modelValidator(k, parsedValue);\n });\n normalize(modelDefinition, draft);\n }, p => (patches = p));\n const hasExistingPatches = modelPatchesMap.has(source);\n if (patches.length || hasExistingPatches) {\n if (hasExistingPatches) {\n const [existingPatches, existingSource] = modelPatchesMap.get(source);\n const mergedPatches = mergePatches(existingSource, existingPatches, patches);\n modelPatchesMap.set(model, [mergedPatches, existingSource]);\n checkReadOnlyPropertyOnUpdate(mergedPatches, modelDefinition);\n }\n else {\n modelPatchesMap.set(model, [patches, source]);\n checkReadOnlyPropertyOnUpdate(patches, modelDefinition);\n }\n }\n else {\n // always register patches when performing a copyOf, even if the\n // patches list is empty. this allows `save()` to recognize when an\n // instance is the result of a `copyOf()`. without more significant\n // refactoring, this is the only way for `save()` to know which\n // diffs (patches) are relevant for `storage` to use in building\n // the list of \"changed\" fields for mutations.\n modelPatchesMap.set(model, [[], source]);\n }\n return attached(model, ModelAttachment.DataStore);\n }\n // \"private\" method (that's hidden via `Setting`) for `withSSRContext` to use\n // to gain access to `modelInstanceCreator` and `clazz` for persisting IDs from server to client.\n static fromJSON(json) {\n if (Array.isArray(json)) {\n return json.map(init => this.fromJSON(init));\n }\n const instance = modelInstanceCreator(clazz, json);\n const modelValidator = validateModelFields(modelDefinition);\n Object.entries(instance).forEach(([k, v]) => {\n modelValidator(k, v);\n });\n return attached(instance, ModelAttachment.DataStore);\n }\n };\n clazz[immerable] = true;\n Object.defineProperty(clazz, 'name', { value: modelDefinition.name });\n // Add getters/setters for relationship fields.\n // getter - for lazy loading\n // setter - for FK management\n const allModelRelationships = ModelRelationship.allFrom({\n builder: clazz,\n schema: modelDefinition,\n pkField: extractPrimaryKeyFieldNames(modelDefinition),\n });\n for (const relationship of allModelRelationships) {\n const { field } = relationship;\n Object.defineProperty(clazz.prototype, modelDefinition.fields[field].name, {\n set(model) {\n if (!(typeof model === 'object' || typeof model === 'undefined'))\n return;\n // if model is undefined or null, the connection should be removed\n if (model) {\n // Avoid validation error when processing AppSync response with nested\n // selection set. Nested entitites lack version field and can not be validated\n // TODO: explore a more reliable method to solve this\n if (Object.prototype.hasOwnProperty.call(model, '_version')) {\n const modelConstructor = Object.getPrototypeOf(model || {})\n .constructor;\n if (!isValidModelConstructor(modelConstructor)) {\n const msg = `Value passed to ${modelDefinition.name}.${field} is not a valid instance of a model`;\n logger.error(msg, { model });\n throw new Error(msg);\n }\n if (modelConstructor.name.toLowerCase() !==\n relationship.remoteModelConstructor.name.toLowerCase()) {\n const msg = `Value passed to ${modelDefinition.name}.${field} is not an instance of ${relationship.remoteModelConstructor.name}`;\n logger.error(msg, { model });\n throw new Error(msg);\n }\n }\n }\n // if the relationship can be managed automagically, set the FK's\n if (relationship.isComplete) {\n for (let i = 0; i < relationship.localJoinFields.length; i++) {\n this[relationship.localJoinFields[i]] =\n model?.[relationship.remoteJoinFields[i]];\n }\n const instanceMemos = modelInstanceAssociationsMap.has(this)\n ? modelInstanceAssociationsMap.get(this)\n : modelInstanceAssociationsMap.set(this, {}).get(this);\n instanceMemos[field] = model || undefined;\n }\n },\n get() {\n /**\n * Bucket for holding related models instances specific to `this` instance.\n */\n const instanceMemos = modelInstanceAssociationsMap.has(this)\n ? modelInstanceAssociationsMap.get(this)\n : modelInstanceAssociationsMap.set(this, {}).get(this);\n // if the memos already has a result for this field, we'll use it.\n // there is no \"cache\" invalidation of any kind; memos are permanent to\n // keep an immutable perception of the instance.\n if (!Object.prototype.hasOwnProperty.call(instanceMemos, field)) {\n // before we populate the memo, we need to know where to look for relatives.\n // today, this only supports DataStore. Models aren't managed elsewhere in Amplify.\n if (getAttachment(this) === ModelAttachment.DataStore) {\n // when we fetch the results using a query constructed under the guidance\n // of the relationship metadata, we DO NOT AWAIT resolution. we want to\n // drop the promise into the memo's synchronously, eliminating the chance\n // for a race.\n const resultPromise = instance.query(relationship.remoteModelConstructor, base => base.and(q => {\n return relationship.remoteJoinFields.map((joinField, index) => {\n // TODO: anything we can use instead of `any` here?\n return q[joinField].eq(this[relationship.localJoinFields[index]]);\n });\n }));\n // results in hand, how we return them to the caller depends on the relationship type.\n if (relationship.type === 'HAS_MANY') {\n // collections should support async iteration, even though we don't\n // leverage it fully [yet].\n instanceMemos[field] = new AsyncCollection(resultPromise);\n }\n else {\n // non-collections should only ever return 1 value *or nothing*.\n // if we have more than 1 record, something's amiss. it's not our job\n // pick a result for the customer. it's our job to say \"something's wrong.\"\n instanceMemos[field] = resultPromise.then(rows => {\n if (rows.length > 1) {\n // should never happen for a HAS_ONE or BELONGS_TO.\n const 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`);\n console.error(err);\n throw err;\n }\n else {\n return rows[0];\n }\n });\n }\n }\n else if (getAttachment(this) === ModelAttachment.API) {\n throw new Error('Lazy loading from API is not yet supported!');\n }\n else {\n if (relationship.type === 'HAS_MANY') {\n return new AsyncCollection([]);\n }\n else {\n return Promise.resolve(undefined);\n }\n }\n }\n return instanceMemos[field];\n },\n });\n }\n return clazz;\n};\n/**\n * An eventually loaded related model instance.\n */\nexport class AsyncItem extends Promise {\n}\n/**\n * A collection of related model instances.\n *\n * This collection can be async-iterated or turned directly into an array using `toArray()`.\n */\nexport class AsyncCollection {\n constructor(values) {\n this.values = values;\n }\n /**\n * Facilitates async iteration.\n *\n * ```ts\n * for await (const item of collection) {\n * handle(item)\n * }\n * ```\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of\n */\n [Symbol.asyncIterator]() {\n let values;\n let index = 0;\n return {\n next: async () => {\n if (!values)\n values = await this.values;\n if (index < values.length) {\n const result = {\n value: values[index],\n done: false,\n };\n index++;\n return result;\n }\n return {\n value: null,\n done: true,\n };\n },\n };\n }\n /**\n * Turns the collection into an array, up to the amount specified in `max` param.\n *\n * ```ts\n * const all = await collection.toArray();\n * const first100 = await collection.toArray({max: 100});\n * ```\n */\n async toArray({ max = Number.MAX_SAFE_INTEGER, } = {}) {\n const output = [];\n let i = 0;\n for await (const element of this) {\n if (i < max) {\n output.push(element);\n i++;\n }\n else {\n break;\n }\n }\n return output;\n }\n}\nconst checkReadOnlyPropertyOnCreate = (draft, modelDefinition) => {\n const modelKeys = Object.keys(draft);\n const { fields } = modelDefinition;\n modelKeys.forEach(key => {\n if (fields[key] && fields[key].isReadOnly) {\n throw new Error(`${key} is read-only.`);\n }\n });\n};\nconst checkReadOnlyPropertyOnUpdate = (patches, modelDefinition) => {\n const patchArray = patches.map(p => [p.path[0], p.value]);\n const { fields } = modelDefinition;\n patchArray.forEach(([key, val]) => {\n if (!val || !fields[key])\n return;\n if (fields[key].isReadOnly) {\n throw new Error(`${key} is read-only.`);\n }\n });\n};\nconst createNonModelClass = (typeDefinition) => {\n const clazz = class Model {\n constructor(init) {\n const instance = produce(this, (draft) => {\n initializeInstance(init, typeDefinition, draft);\n });\n return instance;\n }\n };\n clazz[immerable] = true;\n Object.defineProperty(clazz, 'name', { value: typeDefinition.name });\n registerNonModelClass(clazz);\n return clazz;\n};\nfunction isQueryOne(obj) {\n return typeof obj === 'string';\n}\nfunction defaultConflictHandler(conflictData) {\n const { localModel, modelConstructor, remoteModel } = conflictData;\n const { _version } = remoteModel;\n return modelInstanceCreator(modelConstructor, { ...localModel, _version });\n}\nfunction defaultErrorHandler(error) {\n logger.warn(error);\n}\nfunction getModelConstructorByModelName(namespaceName, modelName) {\n let result;\n switch (namespaceName) {\n case DATASTORE:\n result = dataStoreClasses[modelName];\n break;\n case USER:\n result = userClasses[modelName];\n break;\n case SYNC:\n result = syncClasses[modelName];\n break;\n case STORAGE:\n result = storageClasses[modelName];\n break;\n default:\n throw new Error(`Invalid namespace: ${namespaceName}`);\n }\n if (isValidModelConstructor(result)) {\n return result;\n }\n else {\n const msg = `Model name is not valid for namespace. modelName: ${modelName}, namespace: ${namespaceName}`;\n logger.error(msg);\n throw new Error(msg);\n }\n}\n/**\n * Queries the DataStore metadata tables to see if they are the expected\n * version. If not, clobbers the whole DB. If so, leaves them alone.\n * Otherwise, simply writes the schema version.\n *\n * SIDE EFFECT:\n * 1. Creates a transaction\n * 1. Updates data.\n *\n * @param storage Storage adapter containing the metadata.\n * @param version The expected schema version.\n */\nasync function checkSchemaVersion(storage, version) {\n const SettingCtor = dataStoreClasses.Setting;\n const modelDefinition = schema.namespaces[DATASTORE].models.Setting;\n await storage.runExclusive(async (s) => {\n const [schemaVersionSetting] = await s.query(SettingCtor, ModelPredicateCreator.createFromAST(modelDefinition, {\n and: { key: { eq: SETTING_SCHEMA_VERSION } },\n }), { page: 0, limit: 1 });\n if (schemaVersionSetting !== undefined &&\n schemaVersionSetting.value !== undefined) {\n const storedValue = JSON.parse(schemaVersionSetting.value);\n if (storedValue !== version) {\n await s.clear(false);\n }\n }\n else {\n await s.save(modelInstanceCreator(SettingCtor, {\n key: SETTING_SCHEMA_VERSION,\n value: JSON.stringify(version),\n }));\n }\n });\n}\nlet syncSubscription;\nfunction getNamespace() {\n const namespace = {\n name: DATASTORE,\n relationships: {},\n enums: {},\n nonModels: {},\n models: {\n Setting: {\n name: 'Setting',\n pluralName: 'Settings',\n syncable: false,\n fields: {\n id: {\n name: 'id',\n type: 'ID',\n isRequired: true,\n isArray: false,\n },\n key: {\n name: 'key',\n type: 'String',\n isRequired: true,\n isArray: false,\n },\n value: {\n name: 'value',\n type: 'String',\n isRequired: true,\n isArray: false,\n },\n },\n },\n },\n };\n return namespace;\n}\nvar DataStoreState;\n(function (DataStoreState) {\n DataStoreState[\"NotRunning\"] = \"Not Running\";\n DataStoreState[\"Starting\"] = \"Starting\";\n DataStoreState[\"Running\"] = \"Running\";\n DataStoreState[\"Stopping\"] = \"Stopping\";\n DataStoreState[\"Clearing\"] = \"Clearing\";\n})(DataStoreState || (DataStoreState = {}));\n// TODO: How can we get rid of the non-null assertions?\n// https://github.com/aws-amplify/amplify-js/pull/10477/files#r1007363485\nclass DataStore {\n constructor() {\n // reference to configured category instances. Used for preserving SSR context\n this.InternalAPI = InternalAPI;\n this.Cache = Cache;\n // Non-null assertions (bang operator) have been added to most of these properties\n // to make TS happy. These properties are all expected to be set immediately after\n // construction.\n // TODO: Refactor to use proper DI if possible. If not possible, change these to\n // optionals and implement conditional checks throughout. Rinse/repeat on all\n // sync engine processors, storage engine, adapters, etc..\n this.amplifyConfig = {};\n this.syncPredicates = new WeakMap();\n // object that gets passed to descendent classes. Allows us to pass these down by reference\n this.amplifyContext = {\n InternalAPI: this.InternalAPI,\n };\n /**\n * **IMPORTANT!**\n *\n * Accumulator for background things that can **and MUST** be called when\n * DataStore stops.\n *\n * These jobs **MUST** be *idempotent promises* that resolve ONLY\n * once the intended jobs are completely finished and/or otherwise destroyed\n * and cleaned up with ZERO outstanding:\n *\n * 1. side effects (e.g., state changes)\n * 1. callbacks\n * 1. subscriptions\n * 1. calls to storage\n * 1. *etc.*\n *\n * Methods that create pending promises, subscriptions, callbacks, or any\n * type of side effect **MUST** be registered with the manager. And, a new\n * manager must be created after each `exit()`.\n *\n * Failure to comply will put DataStore into a highly unpredictable state\n * when it needs to stop or clear -- which occurs when restarting with new\n * sync expressions, during testing, and potentially during app code\n * recovery handling, etc..\n *\n * It is up to the discretion of each disposer whether to wait for job\n * completion or to cancel operations and issue failures *as long as the\n * disposer returns in a reasonable amount of time.*\n *\n * (Reasonable = *seconds*, not minutes.)\n */\n this.runningProcesses = new BackgroundProcessManager();\n /**\n * Indicates what state DataStore is in.\n *\n * Not [yet?] used for actual state management; but for messaging\n * when errors occur, to help troubleshoot.\n */\n this.state = DataStoreState.NotRunning;\n /**\n * If not already done:\n * 1. Attaches and initializes storage.\n * 2. Loads the schema and records metadata.\n * 3. If `this.amplifyConfig.aws_appsync_graphqlEndpoint` contains a URL,\n * attaches a sync engine, starts it, and subscribes.\n */\n this.start = async () => {\n return this.runningProcesses\n .add(async () => {\n this.state = DataStoreState.Starting;\n if (this.initialized === undefined) {\n logger.debug('Starting DataStore');\n this.initialized = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n }\n else {\n await this.initialized;\n return;\n }\n this.storage = new Storage(schema, namespaceResolver, getModelConstructorByModelName, modelInstanceCreator, this.storageAdapter, this.sessionId);\n await this.storage.init();\n checkSchemaInitialized();\n await checkSchemaVersion(this.storage, schema.version);\n const { aws_appsync_graphqlEndpoint } = this.amplifyConfig;\n if (aws_appsync_graphqlEndpoint) {\n logger.debug('GraphQL endpoint available', aws_appsync_graphqlEndpoint);\n this.syncPredicates = await this.processSyncExpressions();\n this.sync = new SyncEngine(schema, namespaceResolver, syncClasses, userClasses, this.storage, modelInstanceCreator, this.conflictHandler, this.errorHandler, this.syncPredicates, this.amplifyConfig, this.authModeStrategy, this.amplifyContext, this.connectivityMonitor);\n const fullSyncIntervalInMilliseconds = this.fullSyncInterval * 1000 * 60; // fullSyncInterval from param is in minutes\n syncSubscription = this.sync\n .start({ fullSyncInterval: fullSyncIntervalInMilliseconds })\n .subscribe({\n next: ({ type, data }) => {\n /**\n * In Node, we need to wait for queries to be synced to prevent returning empty arrays.\n * In non-Node environments (the browser or React Native), we can begin returning data\n * once subscriptions are in place.\n */\n const readyType = isNode()\n ? ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY\n : ControlMessage.SYNC_ENGINE_STORAGE_SUBSCRIBED;\n if (type === readyType) {\n this.initResolve();\n }\n Hub.dispatch('datastore', {\n event: type,\n data,\n });\n },\n error: err => {\n logger.warn('Sync error', err);\n this.initReject();\n },\n });\n }\n else {\n logger.warn(\"Data won't be synchronized. No GraphQL endpoint configured. Did you forget `Amplify.configure(awsconfig)`?\", {\n config: this.amplifyConfig,\n });\n this.initResolve();\n }\n await this.in