UNPKG

electrodb-temp

Version:

A library to more easily create and interact with multiple entities and heretical relationships in dynamodb

1,777 lines (1,622 loc) 151 kB
"use strict"; const { Schema } = require("./schema"); const { AllPages, KeyCasing, TableIndex, FormatToReturnValues, ReturnValues, EntityVersions, ItemOperations, UnprocessedTypes, Pager, ElectroInstance, KeyTypes, QueryTypes, MethodTypes, Comparisons, ExpressionTypes, ModelVersions, ElectroInstanceTypes, MaxBatchItems, TerminalOperation, ResultOrderOption, ResultOrderParam, IndexTypes, KeyAttributesComparisons, MethodTypeTranslation, TransactionCommitSymbol, CastKeyOptions, ComparisonTypes, DataOptions, } = require("./types"); const { FilterFactory } = require("./filters"); const { FilterOperations } = require("./operations"); const { WhereFactory } = require("./where"); const { clauses, ChainState } = require("./clauses"); const { EventManager } = require("./events"); const validations = require("./validations"); const c = require("./client"); const u = require("./util"); const e = require("./errors"); const v = require("./validations"); const ImpactedIndexTypeSource = { composite: "composite", provided: "provided", }; class Entity { constructor(model, config = {}) { config = c.normalizeConfig(config); this.eventManager = new EventManager({ listeners: config.listeners, }); this.eventManager.add(config.logger); this._validateModel(model); this.version = EntityVersions.v1; this.config = config; this.client = config.client; this.model = this._parseModel(model, config); /** start beta/v1 condition **/ this.config.table = config.table || model.table; /** end beta/v1 condition **/ this._filterBuilder = new FilterFactory( this.model.schema.attributes, FilterOperations, ); this._whereBuilder = new WhereFactory( this.model.schema.attributes, FilterOperations, ); this._clausesWithFilters = this._filterBuilder.injectFilterClauses( clauses, this.model.filters, ); this._clausesWithFilters = this._whereBuilder.injectWhereClauses( this._clausesWithFilters, ); this.query = {}; for (let accessPattern in this.model.indexes) { let index = this.model.indexes[accessPattern].index; this.query[accessPattern] = (...values) => { const options = { indexType: this.model.indexes[accessPattern].type || IndexTypes.isolated, }; return this._makeChain( index, this._clausesWithFilters, clauses.index, options, ).query(...values); }; } this.config.identifiers = config.identifiers || {}; this.identifiers = { entity: this.config.identifiers.entity || "__edb_e__", version: this.config.identifiers.version || "__edb_v__", }; this._instance = ElectroInstance.entity; this._instanceType = ElectroInstanceTypes.entity; this.schema = model; } get scan() { return this._makeChain( TableIndex, this._clausesWithFilters, clauses.index, { _isPagination: true }, ).scan(); } setIdentifier(type = "", identifier = "") { if (!this.identifiers[type]) { throw new e.ElectroError( e.ErrorCodes.InvalidIdentifier, `Invalid identifier type: "${type}". Valid identifiers include: ${u.commaSeparatedString( Object.keys(this.identifiers), )}`, ); } else { this.identifiers[type] = identifier; } } getName() { return this.model.entity; } getVersion() { return this.model.version; } ownsItem(item) { return ( item && this.getName() === item[this.identifiers.entity] && this.getVersion() === item[this.identifiers.version] && validations.isStringHasLength(item[this.identifiers.entity]) && validations.isStringHasLength(item[this.identifiers.version]) ); } _attributesIncludeKeys(attributes = []) { let { pk, sk } = this.model.prefixes[TableIndex]; let pkFound = false; let skFound = false; for (let i = 0; i < attributes.length; i++) { const attribute = attributes[i]; if (attribute === sk.field) { skFound = true; } if (attribute === pk.field) { skFound = true; } if (pkFound && skFound) { return true; } } return false; } ownsKeys(key = {}) { let { pk, sk } = this.model.prefixes[TableIndex]; let hasSK = this.model.lookup.indexHasSortKeys[TableIndex]; const typeofPkProvided = typeof key[pk.field]; const pkPrefixMatch = typeofPkProvided === "string" && key[pk.field].startsWith(pk.prefix); const isNumericPk = typeofPkProvided === "number" && pk.cast === "number"; let pkMatch = pkPrefixMatch || isNumericPk; let skMatch = pkMatch && !hasSK; if (pkMatch && hasSK) { const typeofSkProvided = typeof key[sk.field]; const skPrefixMatch = typeofSkProvided === "string" && key[sk.field].startsWith(sk.prefix); const isNumericSk = typeofSkProvided === "number" && sk.cast === "number"; skMatch = skPrefixMatch || isNumericSk; } return ( pkMatch && skMatch && this._formatKeysToItem(TableIndex, key) !== null ); } ownsCursor(cursor) { if (typeof cursor === "string") { cursor = u.cursorFormatter.deserialize(cursor); } return this.ownsKeys(cursor); } serializeCursor(key) { return u.cursorFormatter.serialize(key); } deserializeCursor(cursor) { return u.cursorFormatter.deserialize(cursor); } /** @depricated pagers no longer exist, use the new cursor api */ ownsPager(pager, index = TableIndex) { if (pager === null) { return false; } let tableIndexFacets = this.model.facets.byIndex[index]; // todo: is the fact it doesn't use the provided index a bug? // feels like collections may have played a roll into why this is this way let indexFacets = this.model.facets.byIndex[index]; // Unknown index if (tableIndexFacets === undefined || indexFacets === undefined) { return false; } // Should match all primary index facets let matchesTableIndex = tableIndexFacets.all.every((facet) => { return pager[facet.name] !== undefined; }); // If the pager doesnt match the table index, exit early if (!matchesTableIndex) { return false; } return indexFacets.all.every((facet) => { return pager[facet.name] !== undefined; }); } match(facets = {}) { const options = { _isPagination: true }; const match = this._findBestIndexKeyMatch(facets); if (match.shouldScan) { return this._makeChain( TableIndex, this._clausesWithFilters, clauses.index, options, ) .scan() .filter((attr) => { let eqFilters = []; for (let facet of Object.keys(facets)) { if (attr[facet] !== undefined && facets[facet] !== undefined) { eqFilters.push(attr[facet].eq(facets[facet])); } } return eqFilters.join(" AND "); }); } else { return this._makeChain( match.index, this._clausesWithFilters, clauses.index, options, ) .query(facets) .filter((attr) => { let eqFilters = []; for (let facet of Object.keys(facets)) { if (attr[facet] !== undefined && facets[facet] !== undefined) { eqFilters.push(attr[facet].eq(facets[facet])); } } return eqFilters.join(" AND "); }); } } find(facets = {}) { const options = { _isPagination: true }; const match = this._findBestIndexKeyMatch(facets); if (match.shouldScan) { return this._makeChain( TableIndex, this._clausesWithFilters, clauses.index, options, ).scan(); } else { return this._makeChain( match.index, this._clausesWithFilters, clauses.index, options, ).query(facets); } } collection(collection = "", clauses = {}, facets = {}, options = {}) { const chainOptions = { ...options, _isPagination: true, _isCollectionQuery: true, }; let index = this.model.translations.collections.fromCollectionToIndex[collection]; if (index === undefined) { throw new Error(`Invalid collection: ${collection}`); } const chain = this._makeChain(index, clauses, clauses.index, chainOptions); if (options.indexType === IndexTypes.clustered) { return chain.clusteredCollection(collection, facets); } else { return chain.collection(collection, facets); } } _validateModel(model) { return validations.model(model); } check(compositeAttributes = {}) { return this._makeChain( TableIndex, this._clausesWithFilters, clauses.index, ).check(compositeAttributes); } get(facets = {}) { let index = TableIndex; if (Array.isArray(facets)) { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).batchGet(facets); } else { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).get(facets); } } delete(facets = {}) { let index = TableIndex; if (Array.isArray(facets)) { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).batchDelete(facets); } else { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).delete(facets); } } put(attributes = {}) { let index = TableIndex; if (Array.isArray(attributes)) { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).batchPut(attributes); } else { return this._makeChain( index, this._clausesWithFilters, clauses.index, ).put(attributes); } } upsert(attributes = {}) { let index = TableIndex; return this._makeChain( index, this._clausesWithFilters, clauses.index, ).upsert(attributes); } create(attributes = {}) { let index = TableIndex; let options = {}; return this._makeChain( index, this._clausesWithFilters, clauses.index, options, ).create(attributes); } update(facets = {}) { let index = TableIndex; return this._makeChain( index, this._clausesWithFilters, clauses.index, ).update(facets); } patch(facets = {}) { let index = TableIndex; let options = {}; return this._makeChain( index, this._clausesWithFilters, clauses.index, options, ).patch(facets); } remove(facets = {}) { let index = TableIndex; let options = {}; return this._makeChain( index, this._clausesWithFilters, clauses.index, options, ).remove(facets); } async transactWrite(parameters, config) { let response = await this._exec( MethodTypes.transactWrite, parameters, config, ); return response; } async transactGet(parameters, config) { let response = await this._exec( MethodTypes.transactGet, parameters, config, ); return response; } async go(method, parameters = {}, config = {}) { let stackTrace; if (!config.originalErr) { stackTrace = new e.ElectroError(e.ErrorCodes.AWSError); } try { switch (method) { case MethodTypes.batchWrite: return await this.executeBulkWrite(parameters, config); case MethodTypes.batchGet: return await this.executeBulkGet(parameters, config); case MethodTypes.query: case MethodTypes.scan: return await this.executeQuery(method, parameters, config); default: return await this.executeOperation(method, parameters, config); } } catch (err) { if (config.originalErr || stackTrace === undefined) { return Promise.reject(err); } else { if (err.__isAWSError) { stackTrace.message = `Error thrown by DynamoDB client: "${err.message}" - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#aws-error`; stackTrace.cause = err; return Promise.reject(stackTrace); } else if (err.isElectroError) { return Promise.reject(err); } else { stackTrace.message = new e.ElectroError( e.ErrorCodes.UnknownError, err.message, err, ).message; return Promise.reject(stackTrace); } } } } async _exec(method, params, config = {}) { const notifyQuery = () => { this.eventManager.trigger( { type: "query", method, params, config, }, config.listeners, ); }; const notifyResults = (results, success) => { this.eventManager.trigger( { type: "results", method, config, success, results, }, config.listeners, ); }; const dynamoDBMethod = MethodTypeTranslation[method]; return this.client[dynamoDBMethod](params) .promise() .then((results) => { notifyQuery(); notifyResults(results, true); return results; }) .catch((err) => { notifyQuery(); notifyResults(err, false); err.__isAWSError = true; throw err; }); } async executeBulkWrite(parameters, config) { if (!Array.isArray(parameters)) { parameters = [parameters]; } let results = []; let concurrent = this._normalizeConcurrencyValue(config.concurrent); let concurrentOperations = u.batchItems(parameters, concurrent); for (let operation of concurrentOperations) { await Promise.all( operation.map(async (params) => { let response = await this._exec( MethodTypes.batchWrite, params, config, ); if (validations.isFunction(config.parse)) { let parsed = config.parse(config, response); if (parsed) { results.push(parsed); } } else { let { unprocessed } = this.formatBulkWriteResponse( response, config, ); for (let u of unprocessed) { results.push(u); } } }), ); } return { unprocessed: results }; } _createNewBatchGetOrderMaintainer(config = {}) { const pkName = this.model.translations.keys[TableIndex].pk; const skName = this.model.translations.keys[TableIndex].sk; const enabled = !!config.preserveBatchOrder; const table = this.config.table; const keyFormatter = (record = {}) => { const pk = record[pkName]; const sk = record[skName]; return `${pk}${sk}`; }; return new u.BatchGetOrderMaintainer({ table, enabled, keyFormatter, }); } _safeMinimum(...values) { let eligibleNumbers = []; for (let value of values) { if (typeof value === "number") { eligibleNumbers.push(value); } } if (eligibleNumbers.length) { return Math.min(...eligibleNumbers); } return undefined; } async executeBulkGet(parameters, config) { if (!Array.isArray(parameters)) { parameters = [parameters]; } const orderMaintainer = this._createNewBatchGetOrderMaintainer(config); orderMaintainer.defineOrder(parameters); let concurrent = this._normalizeConcurrencyValue(config.concurrent); let concurrentOperations = u.batchItems(parameters, concurrent); let resultsAll = config.preserveBatchOrder ? new Array(orderMaintainer.getSize()).fill(null) : []; let unprocessedAll = []; for (let operation of concurrentOperations) { await Promise.all( operation.map(async (params) => { let response = await this._exec(MethodTypes.batchGet, params, config); if (validations.isFunction(config.parse)) { resultsAll.push(config.parse(config, response)); } else { this.applyBulkGetResponseFormatting({ orderMaintainer, resultsAll, unprocessedAll, response, config, }); } }), ); } return { data: resultsAll, unprocessed: unprocessedAll }; } async hydrate(index, keys = [], config) { const items = []; const validKeys = []; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const item = this._formatKeysToItem(index, key); if (item !== null) { items.push(item); validKeys.push(key); } } const results = await this.get(items).go({ ...config, hydrate: false, parse: undefined, hydrator: undefined, _isCollectionQuery: false, preserveBatchOrder: true, ignoreOwnership: config._providedIgnoreOwnership, }); const unprocessed = []; const data = []; for (let i = 0; i < results.data.length; i++) { const key = validKeys[i]; const item = results.data[i]; if (!item) { if (key) { unprocessed.push(key); } } else { data.push(item); } } return { unprocessed, data, }; } async executeQuery(method, parameters, config = {}) { const indexName = parameters.IndexName; let results = config._isCollectionQuery ? {} : []; let ExclusiveStartKey = this._formatExclusiveStartKey({ indexName, config, }); if (ExclusiveStartKey === null) { ExclusiveStartKey = undefined; } let pages = this._normalizePagesValue(config.pages); let iterations = 0; let count = 0; let hydratedUnprocessed = []; const shouldHydrate = config.hydrate && method === MethodTypes.query; do { let response = await this._exec( method, { ExclusiveStartKey, ...parameters }, config, ); ExclusiveStartKey = response.LastEvaluatedKey; response = this.formatResponse(response, parameters.IndexName, { ...config, data: shouldHydrate && (!config.data || config.data === DataOptions.attributes) ? "includeKeys" : config.data, ignoreOwnership: shouldHydrate || config.ignoreOwnership, }); if (config.data === DataOptions.raw) { return response; } else if (config._isCollectionQuery) { for (const entity in response.data) { let items = response.data[entity]; if (shouldHydrate && items.length) { const hydrated = await config.hydrator( entity, parameters.IndexName, items, config, ); items = hydrated.data; hydratedUnprocessed = hydratedUnprocessed.concat( hydrated.unprocessed, ); } results[entity] = results[entity] || []; results[entity] = [...results[entity], ...items]; } } else if (Array.isArray(response.data)) { let prevCount = count; if (config.count) { count += response.data.length; } let items = response.data; const moreItemsThanRequired = !!config.count && count > config.count; if (moreItemsThanRequired) { items = items.slice(0, config.count - prevCount); } if (shouldHydrate) { const hydrated = await this.hydrate( parameters.IndexName, items, config, ); items = hydrated.data; hydratedUnprocessed = hydratedUnprocessed.concat( hydrated.unprocessed, ); } results = [...results, ...items]; if (moreItemsThanRequired || count === config.count) { const lastItem = results[results.length - 1]; ExclusiveStartKey = this._fromCompositeToKeysByIndex({ indexName, provided: lastItem, }); break; } } else { return response; } iterations++; } while ( ExclusiveStartKey && (pages === AllPages || config.count !== undefined || iterations < pages) && (config.count === undefined || count < config.count) ); const cursor = this._formatReturnPager(config, ExclusiveStartKey); if (shouldHydrate) { return { cursor, data: results, unprocessed: hydratedUnprocessed, }; } return { data: results, cursor }; } async executeOperation(method, parameters, config) { let response = await this._exec(method, parameters, config); switch (parameters.ReturnValues) { case FormatToReturnValues.none: return { data: null }; case FormatToReturnValues.all_new: case FormatToReturnValues.all_old: case FormatToReturnValues.updated_new: case FormatToReturnValues.updated_old: return this.formatResponse(response, TableIndex, config); case FormatToReturnValues.default: default: return this._formatDefaultResponse( method, parameters.IndexName, parameters, config, response, ); } } _formatDefaultResponse(method, index, parameters, config = {}, response) { switch (method) { case MethodTypes.put: case MethodTypes.create: return this.formatResponse(parameters, index, config); case MethodTypes.update: case MethodTypes.patch: case MethodTypes.delete: case MethodTypes.remove: case MethodTypes.upsert: return this.formatResponse(response, index, { ...config, _objectOnEmpty: true, }); default: return this.formatResponse(response, index, config); } } cleanseRetrievedData(item = {}, options = {}) { let data = {}; let names = this.model.schema.translationForRetrieval; for (let [attr, value] of Object.entries(item)) { let name = names[attr]; if (name) { data[name] = value; } else if (options.data === DataOptions.includeKeys) { data[attr] = value; } } return data; } formatBulkWriteResponse(response = {}, config = {}) { if (!response || !response.UnprocessedItems) { return response; } const table = config.table || this.getTableName(); const index = TableIndex; let unprocessed = response.UnprocessedItems[table]; if (Array.isArray(unprocessed) && unprocessed.length) { unprocessed = unprocessed.map((request) => { if (request.PutRequest) { return this.formatResponse(request.PutRequest, index, config).data; } else if (request.DeleteRequest) { if (config.unprocessed === UnprocessedTypes.raw) { return request.DeleteRequest.Key; } else { return this._formatKeysToItem(index, request.DeleteRequest.Key); } } else { throw new Error("Unknown response format"); } }); } else { unprocessed = []; } return { unprocessed }; } applyBulkGetResponseFormatting({ resultsAll, unprocessedAll, orderMaintainer, response = {}, config = {}, }) { const table = config.table || this.getTableName(); const index = TableIndex; if (!response.UnprocessedKeys || !response.Responses) { throw new Error("Unknown response format"); } if ( response.UnprocessedKeys[table] && response.UnprocessedKeys[table].Keys && Array.isArray(response.UnprocessedKeys[table].Keys) ) { for (let value of response.UnprocessedKeys[table].Keys) { if (config && config.unprocessed === UnprocessedTypes.raw) { unprocessedAll.push(value); } else { unprocessedAll.push(this._formatKeysToItem(index, value)); } } } if (response.Responses[table] && Array.isArray(response.Responses[table])) { const responses = response.Responses[table]; for (let i = 0; i < responses.length; i++) { const item = responses[i]; const slot = orderMaintainer.getOrder(item); const formatted = this.formatResponse({ Item: item }, index, config); if (slot !== -1) { resultsAll[slot] = formatted.data; } else { resultsAll.push(formatted.data); } } } } formatResponse(response, index, config = {}) { let stackTrace; if (!config.originalErr) { stackTrace = new e.ElectroError(e.ErrorCodes.AWSError); } try { let results = {}; if (validations.isFunction(config.parse)) { results = config.parse(config, response); } else if (config.data === DataOptions.raw && !config._isPagination) { if (response.TableName) { results = {}; } else { results = response; } } else if ( config.data === DataOptions.raw && (config._isPagination || config.lastEvaluatedKeyRaw) ) { results = response; } else { if (response.Item) { if ( (config.ignoreOwnership && config.attributes && config.attributes.length > 0 && !this._attributesIncludeKeys(config.attributes)) || ((config.ignoreOwnership || config.hydrate) && this.ownsKeys(response.Item)) || this.ownsItem(response.Item) ) { results = this.model.schema.formatItemForRetrieval( response.Item, config, ); if (Object.keys(results).length === 0) { results = null; } } else if (!config._objectOnEmpty) { results = null; } } else if (response.Items) { results = []; for (let item of response.Items) { if ( (config.ignoreOwnership && config.attributes && config.attributes.length > 0 && !this._attributesIncludeKeys(config.attributes)) || ((config.ignoreOwnership || config.hydrate) && this.ownsKeys(item)) || this.ownsItem(item) ) { let record = this.model.schema.formatItemForRetrieval( item, config, ); if (Object.keys(record).length > 0) { results.push(record); } } } } else if (response.Attributes) { results = this.model.schema.formatItemForRetrieval( response.Attributes, config, ); if (Object.keys(results).length === 0) { results = null; } } else if (config._objectOnEmpty) { return { data: { ...config._includeOnResponseItem, }, }; } else { results = null; } } if (config._isPagination || response.LastEvaluatedKey) { const nextPage = this._formatReturnPager( config, response.LastEvaluatedKey, ); return { cursor: nextPage || null, data: results }; } return { data: results }; } catch (err) { if ( config.originalErr || stackTrace === undefined || err.isElectroError ) { throw err; } else { stackTrace.message = `Error thrown by DynamoDB client: "${err.message}" - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#aws-error`; stackTrace.cause = err; throw stackTrace; } } } parse(item, options = {}) { if (item === undefined || item === null) { return null; } const config = { ...(options || {}), ignoreOwnership: true, }; return this.formatResponse(item, TableIndex, config); } _fromCompositeToKeys({ provided }, options = {}) { if (!provided || Object.keys(provided).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCompositeProvided, "Invalid conversion composite provided", ); } let keys = {}; const secondaryIndexStrictMode = options.strict === "all" || options.strict === "pk" ? "pk" : "none"; for (const { index } of Object.values(this.model.indexes)) { const indexKeys = this._fromCompositeToKeysByIndex( { indexName: index, provided }, { strict: index === TableIndex ? options.strict : secondaryIndexStrictMode, }, ); if (indexKeys) { keys = { ...keys, ...indexKeys, }; } } if (Object.keys(keys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCompositeProvided, "Invalid conversion composite provided", ); } return keys; } _fromCompositeToCursor({ provided }, options = {}) { const keys = this._fromCompositeToKeys({ provided }, options); if (!keys || Object.keys(keys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCompositeProvided, "Invalid conversion composite provided", ); } return u.cursorFormatter.serialize(keys); } _fromKeysToCursor({ provided }, options = {}) { if (!provided || Object.keys(provided).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, "Invalid keys provided", ); } return u.cursorFormatter.serialize(provided); } _fromKeysToComposite({ provided }, options = {}) { if (!provided || Object.keys(provided).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, "Provided keys could not be used to form composite attributes", ); } let keys = {}; for (const { index } of Object.values(this.model.indexes)) { const composite = this._fromKeysToCompositeByIndex( { indexName: index, provided }, options, ); if (composite) { for (const attribute in composite) { if (keys[attribute] === undefined) { keys[attribute] = composite[attribute]; } } } } if (Object.keys(keys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, "Provided keys could not be used to form composite attributes", ); } return keys; } _fromCursorToKeys({ provided }, options = {}) { if (typeof provided !== "string") { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCursorProvided, "Invalid conversion cursor provided", ); } return u.cursorFormatter.deserialize(provided); } _fromCursorToComposite({ provided }, options = {}) { if (typeof provided !== "string") { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCursorProvided, "Invalid conversion cursor provided", ); } const keys = this._fromCursorToKeys({ provided }, options); if (!keys) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCursorProvided, "Invalid conversion cursor provided", ); } return this._fromKeysToComposite({ provided: keys }, options); } _fromCompositeToCursorByIndex( { indexName = TableIndex, provided }, options = {}, ) { if (!provided || Object.keys(provided).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCompositeProvided, "Invalid conversion composite provided", ); } const keys = this._formatSuppliedPager(indexName, provided, { relaxedPk: false, relaxedSk: false, }); return this._fromKeysToCursorByIndex( { indexName, provided: keys }, options, ); } _fromCompositeToKeysByIndex( { indexName = TableIndex, provided }, options = {}, ) { return this._formatSuppliedPager(indexName, provided, { relaxedPk: options.strict !== "pk" && options.strict !== "all", relaxedSk: options.strict !== "all", }); } _fromCursorToKeysByIndex({ provided }, options = {}) { if (typeof provided !== "string" || provided.length < 1) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCursorProvided, "Invalid conversion cursor provided", ); } return u.cursorFormatter.deserialize(provided); } _fromKeysToCursorByIndex({ indexName = TableIndex, provided }, options = {}) { const isValidTableIndex = this._verifyKeys({ indexName: TableIndex, provided, }); const isValidIndex = this._verifyKeys({ indexName, provided }); if (!isValidTableIndex) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, "Provided keys did not include valid properties for the primary index", ); } else if (!isValidIndex) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, `Provided keys did not include valid properties for the index "${indexName}"`, ); } const keys = this._trimKeysToIndex({ indexName, provided }); if (!keys || Object.keys(keys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, `Provided keys not defined`, ); } return u.cursorFormatter.serialize(provided); } _fromKeysToCompositeByIndex( { indexName = TableIndex, provided }, options = {}, ) { let allKeys = {}; const indexKeys = this._deconstructIndex({ index: indexName, keys: provided, }); if (!indexKeys) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, `Provided keys did not include valid properties for the index "${indexName}"`, ); } allKeys = { ...indexKeys, }; let tableKeys; if (indexName !== TableIndex) { tableKeys = this._deconstructIndex({ index: TableIndex, keys: provided }); } if (tableKeys === null) { return allKeys; } allKeys = { ...allKeys, ...tableKeys, }; if (Object.keys(allKeys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, "Provided keys could not be used to form composite attributes", ); } return allKeys; } _fromCursorToCompositeByIndex( { indexName = TableIndex, provided }, options = {}, ) { const keys = this._fromCursorToKeysByIndex( { indexName, provided }, options, ); if (!keys || Object.keys(keys).length === 0) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionCursorProvided, "Invalid conversion cursor provided", ); } return this._fromKeysToCompositeByIndex( { indexName, provided: keys }, options, ); } _trimKeysToIndex({ indexName = TableIndex, provided }) { if (!provided) { return null; } const pkName = this.model.translations.keys[indexName].pk; const skName = this.model.translations.keys[indexName].sk; const tablePKName = this.model.translations.keys[TableIndex].pk; const tableSKName = this.model.translations.keys[TableIndex].sk; const keys = { [pkName]: provided[pkName], [skName]: provided[skName], [tablePKName]: provided[tablePKName], [tableSKName]: provided[tableSKName], }; if (!keys || Object.keys(keys).length === 0) { return null; } return keys; } _verifyKeys({ indexName, provided }) { if (!provided) { throw new e.ElectroError( e.ErrorCodes.InvalidConversionKeysProvided, `Provided keys not defined`, ); } const pkName = this.model.translations.keys[indexName].pk; const skName = this.model.translations.keys[indexName].sk; return ( provided[pkName] !== undefined && (!skName || provided[skName] !== undefined) ); } _formatReturnPager(config, lastEvaluatedKey) { let page = lastEvaluatedKey || null; if (config.data === DataOptions.raw || config.pager === Pager.raw) { return page; } return config.formatCursor.serialize(page) || null; } _formatExclusiveStartKey({ config, indexName = TableIndex }) { let exclusiveStartKey = config.cursor; if (config.data === DataOptions.raw || config.pager === Pager.raw) { return ( this._trimKeysToIndex({ provided: exclusiveStartKey, indexName }) || null ); } let keys; if (config.pager === Pager.item) { keys = this._fromCompositeToKeysByIndex({ indexName, provided: exclusiveStartKey, }); } else { keys = config.formatCursor.deserialize(exclusiveStartKey); } if (!keys) { return null; } return this._trimKeysToIndex({ provided: keys, indexName }) || null; } setClient(client) { if (client) { this.client = c.normalizeClient(client); } } setTableName(tableName) { this.config.table = tableName; } getTableName() { return this.config.table; } getTableName() { return this.config.table; } _chain(state, clauses, clause) { let current = {}; for (let child of clause.children) { current[child] = (...args) => { state.prev = state.self; state.self = child; let results = clauses[child].action(this, state, ...args); if (clauses[child].children.length) { return this._chain(results, clauses, clauses[child]); } else { return results; } }; } return current; } /* istanbul ignore next */ _makeChain(index = TableIndex, clauses, rootClause, options = {}) { let state = new ChainState({ index, options, attributes: options.attributes || this.model.schema.attributes, hasSortKey: options.hasSortKey || this.model.lookup.indexHasSortKeys[index], compositeAttributes: options.compositeAttributes || this.model.facets.byIndex[index], }); return state.init(this, clauses, rootClause); } _regexpEscape(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } _normalizeConcurrencyValue(value = 1) { value = parseInt(value); if (isNaN(value) || value < 1) { throw new e.ElectroError( e.ErrorCodes.InvalidConcurrencyOption, "Query option 'concurrency' must be of type 'number' and greater than zero.", ); } return value; } _normalizePagesValue(value) { if (value === AllPages) { return value; } value = parseInt(value); if (isNaN(value) || value < 1) { throw new e.ElectroError( e.ErrorCodes.InvalidPagesOption, `Query option 'pages' must be of type 'number' and greater than zero or the string value '${AllPages}'`, ); } return value; } _normalizeLimitValue(value) { if (value !== undefined) { value = parseInt(value); if (isNaN(value) || value < 1) { throw new e.ElectroError( e.ErrorCodes.InvalidLimitOption, "Query option 'limit' must be of type 'number' and greater than zero.", ); } } return value; } _createKeyDeconstructor(prefixes = {}, labels = [], attributes = {}) { let { prefix, isCustom, postfix, cast } = prefixes; let names = []; let types = []; let pattern = `^${this._regexpEscape(prefix || "")}`; for (let { name, label } of labels) { let attr = attributes[name]; if (isCustom && !name && label) { // this case is for when someone uses a direct attribute reference but with a postfix (zoinks ;P) pattern += `${this._regexpEscape(label)}`; } else if (isCustom) { pattern += `${this._regexpEscape( label === undefined ? "" : label, )}(.+)`; } else { pattern += `#${this._regexpEscape( label === undefined ? name : label, )}_(.+)`; } names.push(name); if (attr) { types.push(attr.type); } } if (typeof postfix === "string") { pattern += this._regexpEscape(postfix); } pattern += "$"; let regex = new RegExp(pattern, "i"); return ({ key } = {}) => { const typeofKey = typeof key; if (!["string", "number"].includes(typeofKey)) { return null; } key = `${key}`; const isNumeric = cast === CastKeyOptions.number && typeofKey === "number"; let match = key.match(regex); let results = {}; if (match || isNumeric) { for (let i = 0; i < names.length; i++) { let keyName = names[i]; let value = isNumeric ? key : match[i + 1]; let type = types[i]; switch (type) { case "number": { value = parseFloat(value); break; } case "boolean": { value = value === "true"; break; } } if (keyName && value !== undefined) { results[keyName] = value; } } } else { results = null; } return results; }; } _deconstructIndex({ index = TableIndex, keys = {} } = {}) { const hasIndex = !!this.model.translations.keys[index]; if (!hasIndex) { return null; } let pkName = this.model.translations.keys[index].pk; let skName = this.model.translations.keys[index].sk; const indexHasSortKey = this.model.lookup.indexHasSortKeys[index]; const deconstructors = this.model.keys.deconstructors[index]; const pk = keys[pkName]; if (pk === undefined) { return null; } const pkComposites = deconstructors.pk({ key: pk }); if (pkComposites === null) { return null; } let skComposites = {}; if (indexHasSortKey) { const sk = keys[skName]; if (sk === undefined) { return null; } skComposites = deconstructors.sk({ key: sk }); if (skComposites === null) { return null; } } return { ...pkComposites, ...skComposites, }; } _formatKeysToItem(index = TableIndex, keys) { if ( keys === null || typeof keys !== "object" || Object.keys(keys).length === 0 ) { return keys; } let tableIndex = TableIndex; let indexParts = this._deconstructIndex({ index, keys }); if (indexParts === null) { return null; } // lastEvaluatedKeys from query calls include the index pk/sk as well as the table index's pk/sk if (index !== tableIndex) { const tableIndexParts = this._deconstructIndex({ index: tableIndex, keys, }); if (tableIndexParts === null) { return null; } indexParts = { ...indexParts, ...tableIndexParts }; } let noPartsFound = Object.keys(indexParts).length === 0 && this.model.facets.byIndex[tableIndex].all.length > 0; let partsAreIncomplete = this.model.facets.byIndex[tableIndex].all.find( (facet) => indexParts[facet.name] === undefined, ); if (noPartsFound || partsAreIncomplete) { // In this case no suitable record could be found be the deconstructed pager. // This can be valid in cases where a scan is performed but returns no results. return null; } return indexParts; } _constructPagerIndex(index = TableIndex, item, options = {}) { let pkAttributes = options.relaxedPk ? item : this._expectFacets(item, this.model.facets.byIndex[index].pk); let skAttributes = options.relaxedSk ? item : this._expectFacets(item, this.model.facets.byIndex[index].sk); let keys = this._makeIndexKeys({ index, pkAttributes, skAttributes: [skAttributes], }); return this._makeParameterKey(index, keys.pk, ...keys.sk); } _formatSuppliedPager(index = TableIndex, item, options = {}) { if (typeof item !== "object" || Object.keys(item).length === 0) { return item; } let tableIndex = TableIndex; let pager = this._constructPagerIndex(index, item, options); if (index !== tableIndex) { pager = { ...pager, ...this._constructPagerIndex(tableIndex, item, options), }; } return pager; } _normalizeExecutionOptions({ provided = [], context = {} } = {}) { let config = { includeKeys: false, originalErr: false, raw: false, params: {}, page: {}, lastEvaluatedKeyRaw: false, table: undefined, concurrent: undefined, parse: undefined, pager: Pager.named, unprocessed: UnprocessedTypes.item, response: "default", cursor: null, data: "attributes", consistent: undefined, compare: ComparisonTypes.keys, complete: false, ignoreOwnership: !!this.config.ignoreOwnership, _providedIgnoreOwnership: false, _isPagination: false, _isCollectionQuery: false, pages: 1, count: undefined, listeners: [], preserveBatchOrder: false, attributes: [], terminalOperation: undefined, formatCursor: u.cursorFormatter, order: undefined, hydrate: false, hydrator: (_entity, _indexName, items) => items, _includeOnResponseItem: {}, }; return provided.filter(Boolean).reduce((config, option) => { if (typeof option.order === "string") { switch (option.order.toLowerCase()) { case "asc": config.params[ResultOrderParam] = ResultOrderOption.asc; break; case "desc": config.params[ResultOrderParam] = ResultOrderOption.desc; break; default: throw new e.ElectroError( e.ErrorCodes.InvalidOptions, `Invalid value for query option "order" provided. Valid options include 'asc' and 'desc, received: "${option.order}"`, ); } } if (typeof option.compare === "string") { const type = ComparisonTypes[option.compare.toLowerCase()]; if (type) { config.compare = type; if (type === ComparisonTypes.v2 && option.complete === undefined) { config.complete = true; } } else { throw new e.ElectroError( e.ErrorCodes.InvalidOptions, `Invalid value for query option "compare" provided. Valid options include ${u.commaSeparatedString( Object.keys(ComparisonTypes), )}, received: "${option.compare}"`, ); } } if (typeof option.response === "string" && option.response.length) { const format = ReturnValues[option.response]; if (format === undefined) { throw new e.ElectroError( e.ErrorCodes.InvalidOptions, `Invalid value for query option "format" provided: "${ option.format }". Allowed values include ${u.commaSeparatedString( Object.keys(ReturnValues), )}.`, ); } else if (format !== ReturnValues.default) { config.response = format; if (context.operation === MethodTypes.transactWrite) { config.params.ReturnValuesOnConditionCheckFailure = FormatToReturnValues[format]; } else { config.params.ReturnValues = FormatToReturnValues[format]; } } } if (option.formatCursor) { const isValid = ["serialize", "deserialize"].every( (method) => method in option.formatCursor && validations.isFunction(option.formatCursor[method]), ); if (isValid) { config.formatCursor = option.formatCursor; } else { throw new e.ElectroError( e.ErrorCodes.InvalidOptions, `Invalid value for query option "formatCursor" provided. Formatter interface must have serialize and deserialize functions`, ); } } if (option.terminalOperation in TerminalOperation) { config.terminalOperation = TerminalOperation[option.terminalOperation]; } if (Array.isArray(option.attributes)) { config.attributes = config.attributes.concat(option.attributes); } if (option.preserveBatchOrder === true) { config.preserveBatchOrder = true; } if (option.pages !== undefined) { config.pages = option.pages; } if (option._isCollectionQuery === true) { config._isCollectionQuery = true; } if (option.includeKeys === true) { config.includeKeys = true; } if (option.originalErr === true) { config.originalErr = true; } if (option.raw === true) { config.raw = true; } if (option._isPagination) { config._isPagination = true; } if (option.lastEvaluatedKeyRaw === true) { config.lastEvaluatedKeyRaw = true; config.pager = Pager.raw; config.unprocessed = UnprocessedTypes.raw; } if (option.cursor) { config.cursor = option.cursor; } if (option.data) { if (!DataOptions[option.data]) { throw new e.ElectroError( e.ErrorCodes.InvalidOptions, `Query option 'data' must be one of ${u.commaSeparatedString( Object.keys(DataOptions), )}.`,