@aws-amplify/datastore
Version:
AppSyncLocal support for aws-amplify
899 lines (890 loc) • 35 kB
JavaScript
import { monotonicFactory } from 'ulid';
import { AmplifyUrl, amplifyUuid, WordArray } from '@aws-amplify/core/internals/utils';
import { produce, applyPatches } from 'immer';
import { isPredicateObj, isPredicateGroup, isModelAttributePrimaryKey, SortDirection, isModelAttributeKey, LimitTimerRaceResolvedValues, isModelAttributeCompositeKey } from './types.mjs';
import './predicates/index.mjs';
import { ModelSortPredicateCreator } from './predicates/sort.mjs';
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const ID = 'id';
/**
* Used by the Async Storage Adapter to concatenate key values
* for a record. For instance, if a model has the following keys:
* `customId: ID! @primaryKey(sortKeyFields: ["createdAt"])`,
* we concatenate the `customId` and `createdAt` as:
* `12-234-5#2022-09-28T00:00:00.000Z`
*/
const DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR = '#';
/**
* Used for generating spinal-cased index name from an array of
* key field names.
* E.g. for keys `[id, title]` => 'id-title'
*/
const IDENTIFIER_KEY_SEPARATOR = '-';
const errorMessages = {
idEmptyString: 'An index field cannot contain an empty string value',
queryByPkWithCompositeKeyPresent: 'Models with composite primary keys cannot be queried by a single key value. Use object literal syntax for composite keys instead: https://docs.amplify.aws/lib/datastore/advanced-workflows/q/platform/js/#querying-records-with-custom-primary-keys',
deleteByPkWithCompositeKeyPresent: 'Models with composite primary keys cannot be deleted by a single key value, unless using a predicate. Use object literal syntax for composite keys instead: https://docs.amplify.aws/lib/datastore/advanced-workflows/q/platform/js/#querying-records-with-custom-primary-keys',
observeWithObjectLiteral: 'Object literal syntax cannot be used with observe. Use a predicate instead: https://docs.amplify.aws/lib/datastore/data-access/q/platform/js/#predicates',
};
var NAMESPACES;
(function (NAMESPACES) {
NAMESPACES["DATASTORE"] = "datastore";
NAMESPACES["USER"] = "user";
NAMESPACES["SYNC"] = "sync";
NAMESPACES["STORAGE"] = "storage";
})(NAMESPACES || (NAMESPACES = {}));
const { DATASTORE } = NAMESPACES;
const { USER } = NAMESPACES;
const { SYNC } = NAMESPACES;
const { STORAGE } = NAMESPACES;
const exhaustiveCheck = (obj, throwOnError = true) => {
if (throwOnError) {
throw new Error(`Invalid ${obj}`);
}
};
const isNullOrUndefined = (val) => {
return typeof val === 'undefined' || val === undefined || val === null;
};
const validatePredicate = (model, groupType, predicatesOrGroups) => {
let filterType;
let isNegation = false;
if (predicatesOrGroups.length === 0) {
return true;
}
switch (groupType) {
case 'not':
filterType = 'every';
isNegation = true;
break;
case 'and':
filterType = 'every';
break;
case 'or':
filterType = 'some';
break;
default:
throw new Error(`Invalid ${groupType}`);
}
const result = predicatesOrGroups[filterType](predicateOrGroup => {
if (isPredicateObj(predicateOrGroup)) {
const { field, operator, operand } = predicateOrGroup;
const value = model[field];
return validatePredicateField(value, operator, operand);
}
if (isPredicateGroup(predicateOrGroup)) {
const { type, predicates } = predicateOrGroup;
return validatePredicate(model, type, predicates);
}
throw new Error('Not a predicate or group');
});
return isNegation ? !result : result;
};
const validatePredicateField = (value, operator, operand) => {
switch (operator) {
case 'ne':
return value !== operand;
case 'eq':
return value === operand;
case 'le':
return value <= operand;
case 'lt':
return value < operand;
case 'ge':
return value >= operand;
case 'gt':
return value > operand;
case 'between': {
const [min, max] = operand;
return value >= min && value <= max;
}
case 'beginsWith':
return (!isNullOrUndefined(value) &&
value.startsWith(operand));
case 'contains':
return (!isNullOrUndefined(value) &&
value.indexOf(operand) > -1);
case 'notContains':
return (isNullOrUndefined(value) ||
value.indexOf(operand) ===
-1);
case 'in': {
// Early return if operand is not an array
if (!Array.isArray(operand))
return false;
return operand.includes(value);
}
case 'notIn': {
// Early return if operand is not an array
if (!Array.isArray(operand))
return false;
return !operand.includes(value);
}
default:
return false;
}
};
const isModelConstructor = (obj) => {
return (obj && typeof obj.copyOf === 'function');
};
const nonModelClasses = new WeakSet();
function registerNonModelClass(clazz) {
nonModelClasses.add(clazz);
}
const isNonModelConstructor = (obj) => {
return nonModelClasses.has(obj);
};
const topologicallySortedModels = new WeakMap();
const traverseModel = (srcModelName, instance, namespace, modelInstanceCreator, getModelConstructorByModelName) => {
const modelConstructor = getModelConstructorByModelName(namespace.name, srcModelName);
const result = [];
const newInstance = modelConstructor.copyOf(instance, () => {
// no-op
});
result.unshift({
modelName: srcModelName,
item: newInstance,
instance: newInstance,
});
if (!topologicallySortedModels.has(namespace)) {
topologicallySortedModels.set(namespace, Array.from(namespace.modelTopologicalOrdering.keys()));
}
const sortedModels = topologicallySortedModels.get(namespace);
result.sort((a, b) => {
return (sortedModels.indexOf(a.modelName) - sortedModels.indexOf(b.modelName));
});
return result;
};
let privateModeCheckResult;
const isPrivateMode = () => {
return new Promise(resolve => {
const dbname = amplifyUuid();
// eslint-disable-next-line prefer-const
let db;
const isPrivate = () => {
privateModeCheckResult = false;
resolve(true);
};
const isNotPrivate = async () => {
if (db && db.result && typeof db.result.close === 'function') {
await db.result.close();
}
await indexedDB.deleteDatabase(dbname);
privateModeCheckResult = true;
resolve(false);
};
if (privateModeCheckResult === true) {
return isNotPrivate();
}
if (privateModeCheckResult === false) {
isPrivate();
return;
}
if (indexedDB === null) {
isPrivate();
return;
}
db = indexedDB.open(dbname);
db.onerror = isPrivate;
db.onsuccess = isNotPrivate;
});
};
let safariCompatabilityModeResult;
/**
* Whether the browser's implementation of IndexedDB breaks on array lookups
* against composite indexes whose keypath contains a single column.
*
* E.g., Whether `store.createIndex(indexName, ['id'])` followed by
* `store.index(indexName).get([1])` will *ever* return records.
*
* In all known, modern Safari browsers as of Q4 2022, the query against an index like
* this will *always* return `undefined`. So, the index needs to be created as a scalar.
*/
const isSafariCompatabilityMode = async () => {
try {
const dbName = amplifyUuid();
const storeName = 'indexedDBFeatureProbeStore';
const indexName = 'idx';
if (indexedDB === null)
return false;
if (safariCompatabilityModeResult !== undefined) {
return safariCompatabilityModeResult;
}
const db = await new Promise(resolve => {
const dbOpenRequest = indexedDB.open(dbName);
dbOpenRequest.onerror = () => {
resolve(false);
};
dbOpenRequest.onsuccess = () => {
const openedDb = dbOpenRequest.result;
resolve(openedDb);
};
dbOpenRequest.onupgradeneeded = (event) => {
const upgradedDb = event?.target?.result;
upgradedDb.onerror = () => {
resolve(false);
};
const store = upgradedDb.createObjectStore(storeName, {
autoIncrement: true,
});
store.createIndex(indexName, ['id']);
};
});
if (!db) {
throw new Error('Could not open probe DB');
}
const rwTx = db.transaction(storeName, 'readwrite');
const rwStore = rwTx.objectStore(storeName);
rwStore.add({
id: 1,
});
rwTx.commit();
const result = await new Promise(resolve => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
const getRequest = index.get([1]);
getRequest.onerror = () => {
resolve(false);
};
getRequest.onsuccess = (event) => {
resolve(event?.target?.result);
};
});
if (db && typeof db.close === 'function') {
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
await db.close();
}
await indexedDB.deleteDatabase(dbName);
if (result === undefined) {
safariCompatabilityModeResult = true;
}
else {
safariCompatabilityModeResult = false;
}
}
catch (error) {
safariCompatabilityModeResult = false;
}
return safariCompatabilityModeResult;
};
const HEX_TO_SHORT = {};
for (let i = 0; i < 256; i++) {
let encodedByte = i.toString(16).toLowerCase();
if (encodedByte.length === 1) {
encodedByte = `0${encodedByte}`;
}
HEX_TO_SHORT[encodedByte] = i;
}
const getBytesFromHex = (encoded) => {
if (encoded.length % 2 !== 0) {
throw new Error('Hex encoded strings must have an even number length');
}
const out = new Uint8Array(encoded.length / 2);
for (let i = 0; i < encoded.length; i += 2) {
const encodedByte = encoded.slice(i, i + 2).toLowerCase();
if (encodedByte in HEX_TO_SHORT) {
out[i / 2] = HEX_TO_SHORT[encodedByte];
}
else {
throw new Error(`Cannot decode unrecognized sequence ${encodedByte} as hexadecimal`);
}
}
return out;
};
const randomBytes = (nBytes) => {
const str = new WordArray().random(nBytes).toString();
return getBytesFromHex(str);
};
const prng = () => randomBytes(1)[0] / 0xff;
function monotonicUlidFactory(seed) {
const ulid = monotonicFactory(prng);
return () => {
return ulid(seed);
};
}
/**
* Uses performance.now() if available, otherwise, uses Date.now() (e.g. react native without a polyfill)
*
* The values returned by performance.now() always increase at a constant rate,
* independent of the system clock (which might be adjusted manually or skewed
* by software like NTP).
*
* Otherwise, performance.timing.navigationStart + performance.now() will be
* approximately equal to Date.now()
*
* See: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#Example
*/
function getNow() {
if (typeof performance !== 'undefined' &&
performance &&
typeof performance.now === 'function') {
return performance.now() | 0; // convert to integer
}
else {
return Date.now();
}
}
function sortCompareFunction(sortPredicates) {
return function compareFunction(a, b) {
// enable multi-field sort by iterating over predicates until
// a comparison returns -1 or 1
for (const predicate of sortPredicates) {
const { field, sortDirection } = predicate;
// reverse result when direction is descending
const sortMultiplier = sortDirection === SortDirection.ASCENDING ? 1 : -1;
if (a[field] < b[field]) {
return -1 * sortMultiplier;
}
if (a[field] > b[field]) {
return 1 * sortMultiplier;
}
}
return 0;
};
}
/* deep directed comparison ensuring that all fields on "from" object exist and
* are equal to values on an "against" object
*
* Note: This same guarauntee is not applied for values on "against" that aren't on "from"
*
* @param fromObject - The object that may be an equal subset of the againstObject.
* @param againstObject - The object that may be an equal superset of the fromObject.
*
* @returns True if fromObject is a equal subset of againstObject and False otherwise.
*/
function directedValueEquality(fromObject, againstObject, nullish = false) {
const aKeys = Object.keys(fromObject);
for (const key of aKeys) {
const fromValue = fromObject[key];
const againstValue = againstObject[key];
if (!valuesEqual(fromValue, againstValue, nullish)) {
return false;
}
}
return true;
}
// deep compare any 2 values
// primitives or object types (including arrays, Sets, and Maps)
// returns true if equal by value
// if nullish is true, treat undefined and null values as equal
// to normalize for GQL response values for undefined fields
function valuesEqual(valA, valB, nullish = false) {
let a = valA;
let b = valB;
const nullishCompare = (_a, _b) => {
return ((_a === undefined || _a === null) && (_b === undefined || _b === null));
};
// if one of the values is a primitive and the other is an object
if ((a instanceof Object && !(b instanceof Object)) ||
(!(a instanceof Object) && b instanceof Object)) {
return false;
}
// compare primitive types
if (!(a instanceof Object)) {
if (nullish && nullishCompare(a, b)) {
return true;
}
return a === b;
}
// make sure object types match
if ((Array.isArray(a) && !Array.isArray(b)) ||
(Array.isArray(b) && !Array.isArray(a))) {
return false;
}
if (a instanceof Set && b instanceof Set) {
a = [...a];
b = [...b];
}
if (a instanceof Map && b instanceof Map) {
a = Object.fromEntries(a);
b = Object.fromEntries(b);
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
// last condition is to ensure that [] !== [null] even if nullish. However [undefined] === [null] when nullish
if (aKeys.length !== bKeys.length && (!nullish || Array.isArray(a))) {
return false;
}
// iterate through the longer set of keys
// e.g., for a nullish comparison of a={ a: 1 } and b={ a: 1, b: null }
// we want to iterate through bKeys
const keys = aKeys.length >= bKeys.length ? aKeys : bKeys;
for (const key of keys) {
const aVal = a[key];
const bVal = b[key];
if (!valuesEqual(aVal, bVal, nullish)) {
return false;
}
}
return true;
}
/**
* Statelessly extracts the specified page from an array.
*
* @param records - The source array to extract a page from.
* @param pagination - A definition of the page to extract.
* @returns This items from `records` matching the `pagination` definition.
*/
function inMemoryPagination(records, pagination) {
if (pagination && records.length > 1) {
if (pagination.sort) {
const sortPredicates = ModelSortPredicateCreator.getPredicates(pagination.sort);
if (sortPredicates.length) {
const compareFn = sortCompareFunction(sortPredicates);
records.sort(compareFn);
}
}
const { page = 0, limit = 0 } = pagination;
const start = Math.max(0, page * limit) || 0;
const end = limit > 0 ? start + limit : records.length;
return records.slice(start, end);
}
return records;
}
/**
* An `aysnc` implementation of `Array.some()`. Returns as soon as a match is found.
* @param items The items to check.
* @param matches The async matcher function, expected to
* return Promise<boolean>: `true` for a matching item, `false` otherwise.
* @returns A `Promise<boolean>`, `true` if "some" items match; `false` otherwise.
*/
async function asyncSome(items, matches) {
for (const item of items) {
if (await matches(item)) {
return true;
}
}
return false;
}
/**
* An `aysnc` implementation of `Array.every()`. Returns as soon as a non-match is found.
* @param items The items to check.
* @param matches The async matcher function, expected to
* return Promise<boolean>: `true` for a matching item, `false` otherwise.
* @returns A `Promise<boolean>`, `true` if every item matches; `false` otherwise.
*/
async function asyncEvery(items, matches) {
for (const item of items) {
if (!(await matches(item))) {
return false;
}
}
return true;
}
/**
* An `async` implementation of `Array.filter()`. Returns after all items have been filtered.
* TODO: Return AsyncIterable.
* @param items The items to filter.
* @param matches The `async` matcher function, expected to
* return Promise<boolean>: `true` for a matching item, `false` otherwise.
* @returns A `Promise<T>` of matching items.
*/
async function asyncFilter(items, matches) {
const results = [];
for (const item of items) {
if (await matches(item)) {
results.push(item);
}
}
return results;
}
const isAWSDate = (val) => {
return !!/^\d{4}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2}($|:\d{2}))?$/.exec(val);
};
const isAWSTime = (val) => {
return !!/^\d{2}:\d{2}(:\d{2}(.\d+)?)?(Z|[+-]\d{2}:\d{2}($|:\d{2}))?$/.exec(val);
};
const isAWSDateTime = (val) => {
return !!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(.\d+)?)?(Z|[+-]\d{2}:\d{2}($|:\d{2}))?$/.exec(val);
};
const isAWSTimestamp = (val) => {
return !!/^\d+$/.exec(String(val));
};
const isAWSEmail = (val) => {
return !!/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.exec(val);
};
const isAWSJSON = (val) => {
try {
JSON.parse(val);
return true;
}
catch {
return false;
}
};
const isAWSURL = (val) => {
try {
return !!new AmplifyUrl(val);
}
catch {
return false;
}
};
const isAWSPhone = (val) => {
return !!/^\+?\d[\d\s-]+$/.exec(val);
};
const isAWSIPAddress = (val) => {
return !!/((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$))$/.exec(val);
};
class DeferredPromise {
constructor() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this.promise = new Promise((resolve, reject) => {
self.resolve = resolve;
self.reject = reject;
});
}
}
class DeferredCallbackResolver {
constructor(options) {
this.limitPromise = new DeferredPromise();
this.raceInFlight = false;
this.callback = () => {
// no-op
};
this.defaultErrorHandler = (msg = 'DeferredCallbackResolver error') => {
throw new Error(msg);
};
this.callback = options.callback;
this.errorHandler = options.errorHandler || this.defaultErrorHandler;
this.maxInterval = options.maxInterval || 2000;
}
startTimer() {
this.timerPromise = new Promise((resolve, _reject) => {
this.timer = setTimeout(() => {
resolve(LimitTimerRaceResolvedValues.TIMER);
}, this.maxInterval);
});
}
async racePromises() {
let winner;
try {
this.raceInFlight = true;
this.startTimer();
winner = await Promise.race([
this.timerPromise,
this.limitPromise.promise,
]);
this.callback();
}
catch (err) {
this.errorHandler(err);
}
finally {
// reset for the next race
this.clear();
this.raceInFlight = false;
this.limitPromise = new DeferredPromise();
// eslint-disable-next-line no-unsafe-finally
return winner;
}
}
start() {
if (!this.raceInFlight)
this.racePromises();
}
clear() {
clearTimeout(this.timer);
}
resolve() {
this.limitPromise.resolve(LimitTimerRaceResolvedValues.LIMIT);
}
}
/**
* merge two sets of patches created by immer produce.
* newPatches take precedent over oldPatches for patches modifying the same path.
* In the case many consecutive pathces are merged the original model should
* always be the root model.
*
* Example:
* A -> B, patches1
* B -> C, patches2
*
* mergePatches(A, patches1, patches2) to get patches for A -> C
*
* @param originalSource the original Model the patches should be applied to
* @param oldPatches immer produce patch list
* @param newPatches immer produce patch list (will take precedence)
* @return merged patches
*/
function mergePatches(originalSource, oldPatches, newPatches) {
const patchesToMerge = oldPatches.concat(newPatches);
let patches;
produce(originalSource, draft => {
applyPatches(draft, patchesToMerge);
}, p => {
patches = p;
});
return patches;
}
const getStorename = (namespace, modelName) => {
const storeName = `${namespace}_${modelName}`;
return storeName;
};
// #region Key Utils
/*
When we have GSI(s) with composite sort keys defined on a model
There are some very particular rules regarding which fields must be included in the update mutation input
The field selection becomes more complex as the number of GSIs with composite sort keys grows
To summarize: any time we update a field that is part of the composite sort key of a GSI, we must include:
1. all of the other fields in that composite sort key
2. all of the fields from any other composite sort key that intersect with the fields from 1.
E.g.,
Model @model
@key(name: 'key1' fields: ['hk', 'a', 'b', 'c'])
@key(name: 'key2' fields: ['hk', 'a', 'b', 'd'])
@key(name: 'key3' fields: ['hk', 'x', 'y', 'z'])
Model.a is updated => include ['a', 'b', 'c', 'd']
Model.c is updated => include ['a', 'b', 'c', 'd']
Model.d is updated => include ['a', 'b', 'c', 'd']
Model.x is updated => include ['x', 'y', 'z']
This function accepts a model's attributes and returns grouped sets of composite key fields
Using our example Model above, the function will return:
[
Set('a', 'b', 'c', 'd'),
Set('x', 'y', 'z'),
]
This gives us the opportunity to correctly include the required fields for composite keys
When crafting the mutation input in Storage.getUpdateMutationInput
See 'processCompositeKeys' test in util.test.ts for more examples
*/
const processCompositeKeys = (attributes) => {
const extractCompositeSortKey = ({ properties: {
// ignore the HK (fields[0]) we only need to include the composite sort key fields[1...n]
fields: [, ...sortKeyFields], }, }) => sortKeyFields;
const compositeKeyFields = attributes
.filter(isModelAttributeCompositeKey)
.map(extractCompositeSortKey);
/*
if 2 sets of fields have any intersecting fields => combine them into 1 union set
e.g., ['a', 'b', 'c'] and ['a', 'b', 'd'] => ['a', 'b', 'c', 'd']
*/
const combineIntersecting = (fields) => fields.reduce((combined, sortKeyFields) => {
const sortKeyFieldsSet = new Set(sortKeyFields);
if (combined.length === 0) {
combined.push(sortKeyFieldsSet);
return combined;
}
// does the current set share values with another set we've already added to `combined`?
const intersectingSetIdx = combined.findIndex(existingSet => {
return [...existingSet].some(f => sortKeyFieldsSet.has(f));
});
if (intersectingSetIdx > -1) {
const union = new Set([
...combined[intersectingSetIdx],
...sortKeyFieldsSet,
]);
// combine the current set with the intersecting set we found above
combined[intersectingSetIdx] = union;
}
else {
// none of the sets in `combined` have intersecting values with the current set
combined.push(sortKeyFieldsSet);
}
return combined;
}, []);
const initial = combineIntersecting(compositeKeyFields);
// a single pass pay not be enough to correctly combine all the fields
// call the function once more to get a final merged list of sets
const combined = combineIntersecting(initial);
return combined;
};
const extractKeyIfExists = (modelDefinition) => {
const keyAttribute = modelDefinition?.attributes?.find(isModelAttributeKey);
return keyAttribute;
};
const extractPrimaryKeyFieldNames = (modelDefinition) => {
const keyAttribute = extractKeyIfExists(modelDefinition);
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
return keyAttribute.properties.fields;
}
return [ID];
};
const extractPrimaryKeyValues = (model, keyFields) => {
return keyFields.map(key => model[key]);
};
const extractPrimaryKeysAndValues = (model, keyFields) => {
const primaryKeysAndValues = {};
keyFields.forEach(key => (primaryKeysAndValues[key] = model[key]));
return primaryKeysAndValues;
};
// IdentifierFields<ManagedIdentifier>
// Default behavior without explicit @primaryKey defined
const isIdManaged = (modelDefinition) => {
const keyAttribute = extractKeyIfExists(modelDefinition);
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
return false;
}
return true;
};
// IdentifierFields<OptionallyManagedIdentifier>
// @primaryKey with explicit `id` in the PK. Single key or composite
const isIdOptionallyManaged = (modelDefinition) => {
const keyAttribute = extractKeyIfExists(modelDefinition);
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
return keyAttribute.properties.fields[0] === ID;
}
return false;
};
const establishRelationAndKeys = (namespace) => {
const relationship = {};
const keys = {};
Object.keys(namespace.models).forEach((mKey) => {
relationship[mKey] = { indexes: [], relationTypes: [] };
keys[mKey] = {};
const model = namespace.models[mKey];
Object.keys(model.fields).forEach((attr) => {
const fieldAttribute = model.fields[attr];
if (typeof fieldAttribute.type === 'object' &&
'model' in fieldAttribute.type) {
const { connectionType } = fieldAttribute.association;
relationship[mKey].relationTypes.push({
fieldName: fieldAttribute.name,
modelName: fieldAttribute.type.model,
relationType: connectionType,
targetName: fieldAttribute.association.targetName,
targetNames: fieldAttribute.association.targetNames,
// eslint-disable-next-line dot-notation
associatedWith: fieldAttribute.association['associatedWith'],
});
if (connectionType === 'BELONGS_TO') {
const targetNames = extractTargetNamesFromSrc(fieldAttribute.association);
if (targetNames) {
const idxName = indexNameFromKeys(targetNames);
const idxExists = relationship[mKey].indexes.find(([index]) => index === idxName);
if (!idxExists) {
relationship[mKey].indexes.push([idxName, targetNames]);
}
}
}
}
});
if (model.attributes) {
keys[mKey].compositeKeys = processCompositeKeys(model.attributes);
for (const attribute of model.attributes) {
if (!isModelAttributeKey(attribute)) {
continue;
}
const { fields } = attribute.properties;
if (isModelAttributePrimaryKey(attribute)) {
keys[mKey].primaryKey = fields;
continue;
}
// create indexes for all other keys
const idxName = indexNameFromKeys(fields);
const idxExists = relationship[mKey].indexes.find(([index]) => index === idxName);
if (!idxExists) {
relationship[mKey].indexes.push([idxName, fields]);
}
}
}
// set 'id' as the PK for models without a custom PK explicitly defined
if (!keys[mKey].primaryKey) {
keys[mKey].primaryKey = [ID];
}
// create primary index
relationship[mKey].indexes.push([
'byPk',
keys[mKey].primaryKey,
{ unique: true },
]);
});
return [relationship, keys];
};
const getIndex = (rel, src) => {
let indexName;
// eslint-disable-next-line array-callback-return
rel.some((relItem) => {
if (relItem.modelName === src) {
const targetNames = extractTargetNamesFromSrc(relItem);
indexName = targetNames && indexNameFromKeys(targetNames);
return true;
}
});
return indexName;
};
const getIndexFromAssociation = (indexes, src) => {
let indexName;
if (Array.isArray(src)) {
indexName = indexNameFromKeys(src);
}
else {
indexName = src;
}
const associationIndex = indexes.find(([idxName]) => idxName === indexName);
return associationIndex && associationIndex[0];
};
/**
* Backwards-compatability for schema generated prior to custom primary key support:
the single field `targetName` has been replaced with an array of `targetNames`.
`targetName` and `targetNames` are exclusive (will never exist on the same schema)
* @param src {RelationType | ModelAssociation | undefined}
* @returns array of targetNames, or `undefined`
*/
const extractTargetNamesFromSrc = (src) => {
const targetName = src?.targetName;
const targetNames = src?.targetNames;
if (Array.isArray(targetNames)) {
return targetNames;
}
else if (typeof targetName === 'string') {
return [targetName];
}
else {
return undefined;
}
};
// Generates spinal-cased index name from an array of key field names
// E.g. for keys `[id, title]` => 'id-title'
const indexNameFromKeys = (keys) => {
return keys.reduce((prev, cur, idx) => {
if (idx === 0) {
return cur;
}
return `${prev}${IDENTIFIER_KEY_SEPARATOR}${cur}`;
}, '');
};
const keysEqual = (keysA, keysB) => {
if (keysA.length !== keysB.length) {
return false;
}
return keysA.every((key, idx) => key === keysB[idx]);
};
// Returns primary keys for a model
const getIndexKeys = (namespace, modelName) => {
const keyPath = namespace?.keys?.[modelName]?.primaryKey;
if (keyPath) {
return keyPath;
}
return [ID];
};
// #endregion
/**
* Determine what the managed timestamp field names are for the given model definition
* and return the mapping.
*
* All timestamp fields are included in the mapping, regardless of whether the final field
* names are the defaults or customized in the `@model` directive.
*
* @see https://docs.amplify.aws/cli/graphql/data-modeling/#customize-creation-and-update-timestamps
*
* @param definition modelDefinition to inspect.
* @returns An object mapping `createdAt` and `updatedAt` to their field names.
*/
const getTimestampFields = (definition) => {
const modelAttributes = definition.attributes?.find(attr => attr.type === 'model');
const timestampFieldsMap = modelAttributes?.properties?.timestamps;
const defaultFields = {
createdAt: 'createdAt',
updatedAt: 'updatedAt',
};
const customFields = timestampFieldsMap || {};
return {
...defaultFields,
...customFields,
};
};
export { DATASTORE, DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR, DeferredCallbackResolver, DeferredPromise, ID, IDENTIFIER_KEY_SEPARATOR, NAMESPACES, STORAGE, SYNC, USER, asyncEvery, asyncFilter, asyncSome, directedValueEquality, errorMessages, establishRelationAndKeys, exhaustiveCheck, extractKeyIfExists, extractPrimaryKeyFieldNames, extractPrimaryKeyValues, extractPrimaryKeysAndValues, extractTargetNamesFromSrc, getIndex, getIndexFromAssociation, getIndexKeys, getNow, getStorename, getTimestampFields, inMemoryPagination, indexNameFromKeys, isAWSDate, isAWSDateTime, isAWSEmail, isAWSIPAddress, isAWSJSON, isAWSPhone, isAWSTime, isAWSTimestamp, isAWSURL, isIdManaged, isIdOptionallyManaged, isModelConstructor, isNonModelConstructor, isNullOrUndefined, isPrivateMode, isSafariCompatabilityMode, keysEqual, mergePatches, monotonicUlidFactory, processCompositeKeys, registerNonModelClass, sortCompareFunction, traverseModel, validatePredicate, validatePredicateField, valuesEqual };
//# sourceMappingURL=util.mjs.map