UNPKG

dynamodb-toolbox

Version:

A simple set of tools for working with Amazon DynamoDB and the DocumentClient.

964 lines 56.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const parseTable_js_1 = require("../../lib/parseTable.js"); const expressionBuilder_js_1 = __importDefault(require("../../lib/expressionBuilder.js")); const validateTypes_js_1 = __importDefault(require("../../lib/validateTypes.js")); const Entity_js_1 = __importDefault(require("../Entity/Entity.js")); const projectionBuilder_js_1 = __importDefault(require("../../lib/projectionBuilder.js")); const utils_js_1 = require("../../lib/utils.js"); const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb"); class Table { // Declare constructor (table config and optional entities) constructor(table) { this._execute = true; this._parse = true; this._removeNulls = true; this._entities = []; // Sanity check the table definition if (typeof table !== 'object' || Array.isArray(table)) { (0, utils_js_1.error)('Please provide a valid table definition'); } // Parse the table and merge into this Object.assign(this, (0, parseTable_js_1.parseTable)(table)); } // Sets the auto execute mode (default to true) set autoExecute(val) { this._execute = typeof val === 'boolean' ? val : true; } // Gets the current auto execute mode get autoExecute() { return this._execute; } // Sets the auto parse mode (default to true) set autoParse(val) { this._parse = typeof val === 'boolean' ? val : true; } // Gets the current auto execute mode get autoParse() { return this._parse; } // Sets the auto execute mode (default to true) set removeNullAttributes(val) { this._removeNulls = typeof val === 'boolean' ? val : true; } // Gets the current auto execute mode get removeNullAttributes() { return this._removeNulls; } get DocumentClient() { return this._docClient; } // Validate and sets the document client set DocumentClient(docClient) { var _a, _b, _c, _d; var _e, _f; // @ts-ignore if (docClient && docClient.send) { (_a = (_e = docClient.config).translateConfig) !== null && _a !== void 0 ? _a : (_e.translateConfig = {}); (_b = (_f = docClient.config.translateConfig).marshallOptions) !== null && _b !== void 0 ? _b : (_f.marshallOptions = {}); // Automatically set convertEmptyValues to true, unless false if (((_d = (_c = docClient.config.translateConfig) === null || _c === void 0 ? void 0 : _c.marshallOptions) === null || _d === void 0 ? void 0 : _d.convertEmptyValues) !== false) { docClient.config.translateConfig.marshallOptions.convertEmptyValues = true; } this._docClient = docClient; } else { (0, utils_js_1.error)('Invalid DocumentClient'); } } /** * Adds an entity to the table * @param {Entity|Entity[]} Entity - An Entity or array of Entities to add to the table. * NOTE: this does not adjust the entity's type inference because it is static */ addEntity(entity) { var _a, _b, _c; // Coerce entity to array const entities = Array.isArray(entity) ? entity : [entity]; // Loop through entities for (const i in entities) { const entity = entities[i]; if (!(entity instanceof Entity_js_1.default)) { (0, utils_js_1.error)('Invalid Entity'); } if ((_b = (_a = this._entities) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, entity.name)) { return; } // Generate the reserved words list const reservedWords = Object.getOwnPropertyNames(this).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(this))); // Check for reserved word if (reservedWords.includes(entity.name)) { (0, utils_js_1.error)(`'${entity.name}' is a reserved word and cannot be used to name an Entity`); } // Check for sortKeys (if applicable) if (!this.Table.sortKey && entity.schema.keys.sortKey) { (0, utils_js_1.error)(`${entity.name} entity contains a sortKey, but the Table does not`); } else if (this.Table.sortKey && !entity.schema.keys.sortKey) { (0, utils_js_1.error)(`${entity.name} entity does not have a sortKey defined`); } // Process Entity index keys for (const key in entity.schema.keys) { // Set the value of the key const attr = entity.schema.keys[key]; // Switch based on key type (pk, sk, or index) switch (key) { // For the primary index case 'partitionKey': case 'sortKey': // If the attribute's name doesn't match the table's pk/sk name if (attr !== this.Table[key] && this.Table[key]) { // If the table's index attribute name does not conflict with another entity attribute if (!entity.schema.attributes[this.Table[key]]) { // FIX: better way to do this? // Add the attribute using the same config and add alias entity.schema.attributes[this.Table[key]] = Object.assign({}, entity.schema.attributes[attr], { alias: attr }); // Add a map from the attribute to the new index attribute entity.schema.attributes[attr].map = this.Table[key]; // Otherwise, throw an error } else { (0, utils_js_1.error)(`The Table's ${key} name (${String(this.Table[key])}) conflicts with an Entity attribute name`); } } break; // For secondary indexes default: // Verify that the table has this index if (!this.Table.indexes[key]) (0, utils_js_1.error)(`'${key}' is not a valid secondary index name`); // Loop through the key types (pk/sk) defined in the key mapping for (const keyType in attr) { // Make sure the table index contains the defined key types // @ts-ignore if (!this.Table.indexes[key][keyType]) { (0, utils_js_1.error)(`${entity.name} contains a ${keyType}, but it is not used by ${key}`); } // console.log(key,keyType,this.Table.indexes[key]) // If the attribute's name doesn't match the indexes attribute name // @ts-ignore if (attr[keyType] !== this.Table.indexes[key][keyType]) { // If the indexes attribute name does not conflict with another entity attribute // @ts-ignore if (!entity.schema.attributes[this.Table.indexes[key][keyType]]) { // If there is already a mapping for this attribute, make sure they match // TODO: Figure out if this is even possible anymore. I don't think it is. if (entity.schema.attributes[attr[keyType]].map && entity.schema.attributes[attr[keyType]].map !== // @ts-ignore this.Table.indexes[key][keyType]) { (0, utils_js_1.error)(`${key}'s ${keyType} cannot map to the '${attr[keyType]}' alias because it is already mapped to another table attribute`); } // Add the index attribute using the same config and add alias // @ts-ignore entity.schema.attributes[this.Table.indexes[key][keyType]] = Object.assign({}, entity.schema.attributes[attr[keyType]], { alias: attr[keyType] }); // Add a map from the attribute to the new index attribute // @ts-ignore entity.schema.attributes[attr[keyType]].map = this.Table.indexes[key][keyType]; } else { // @ts-ignore const config = entity.schema.attributes[this.Table.indexes[key][keyType]]; // If the existing attribute isn't used by this index if ((!config.partitionKey && !config.sortKey) || (config.partitionKey && !config.partitionKey.includes(key)) || (config.sortKey && !config.sortKey.includes(key))) { (0, utils_js_1.error)( // @ts-ignore `${key}'s ${keyType} name (${this.Table.indexes[key][keyType]}) conflicts with another Entity attribute name`); } } } } // Check that composite keys define both keys // TODO: This only checks for the attribute, not the explicit assignment if (this.Table.indexes[key].partitionKey && this.Table.indexes[key].sortKey && (!entity.schema.attributes[this.Table.indexes[key].partitionKey] || !entity.schema.attributes[this.Table.indexes[key].sortKey])) { (0, utils_js_1.error)(`${key} requires mappings for both the partitionKey and the sortKey`); } break; } } // Loop through the Entity's attributes and validate their types against the Table definition // Add attribute to table if not defined for (const attr in entity.schema.attributes) { // If an entity field conflicts with the entityField or its alias, throw an error if (this.Table.entityField && (attr === this.Table.entityField || attr === entity._etAlias)) { (0, utils_js_1.error)(`Attribute or alias '${attr}' conflicts with the table's 'entityField' mapping or entity alias`); // If the atribute already exists in the table definition } else if (this.Table.attributes[attr]) { // If type is specified, check for attribute match if (this.Table.attributes[attr].type && this.Table.attributes[attr].type !== entity.schema.attributes[attr].type) { (0, utils_js_1.error)(`${entity.name} attribute type for '${attr}' (${entity.schema.attributes[attr].type}) does not match table's type (${this.Table.attributes[attr].type})`); } // Add entity mappings this.Table.attributes[attr].mappings[entity.name] = Object.assign({ [entity.schema.attributes[attr].alias || attr]: entity.schema.attributes[attr].type, }, // Add setType if type 'set' entity.schema.attributes[attr].type === 'set' ? { _setType: entity.schema.attributes[attr].setType } : {}); // else if the attribute doesn't exist } else if (!entity.schema.attributes[attr].map) { // Add type and entity map this.Table.attributes[attr] = Object.assign({ mappings: { [entity.name]: Object.assign({ [entity.schema.attributes[attr].alias || attr]: entity.schema.attributes[attr].type, }, // Add setType if type 'set' entity.schema.attributes[attr].type === 'set' ? { _setType: entity.schema.attributes[attr].setType } : {}), }, }, entity.schema.attributes[attr].partitionKey || entity.schema.attributes[attr].sortKey ? { type: entity.schema.attributes[attr].type } : null); } } // Add the Entity to the Table's entities list this._entities.push(entity.name); // Add the entity to the Table object this[entity.name] = entity; (_c = entity === null || entity === void 0 ? void 0 : entity.setTable) === null || _c === void 0 ? void 0 : _c.call(entity, this); } } removeEntity(entity) { var _a, _b, _c, _d, _e; if (!(entity instanceof Entity_js_1.default)) { (0, utils_js_1.error)('Entity must be an instance of Entity'); } if (!this._entities.includes(entity.name)) { return; } delete this[entity.name]; // Remove the entity from the table's entity list this._entities.splice(this._entities.indexOf(entity.name), 1); // Loop through the entity's attributes for (const attr in entity.schema.attributes) { // If the attribute is not mapped to another entity if (!entity.schema.attributes[attr].map) { // If the attribute is not used by any other entity if (Object.keys(this.Table.attributes[attr].mappings).length === 1) { // Remove the attribute from the table delete this.Table.attributes[attr]; } else { // Remove the entity from the attribute's mappings delete this.Table.attributes[attr].mappings[entity.name]; } } } let shouldRemoveIndexFromTable = true; for (const indexName in entity.schema.indexes) { for (const tableEntity of this._entities) { if (((_a = this[tableEntity]) === null || _a === void 0 ? void 0 : _a.name) === entity.name) { continue; } if ((_d = (_c = (_b = this[tableEntity]) === null || _b === void 0 ? void 0 : _b.schema) === null || _c === void 0 ? void 0 : _c.indexes) === null || _d === void 0 ? void 0 : _d[indexName]) { shouldRemoveIndexFromTable = false; break; } } if (shouldRemoveIndexFromTable) { delete this.Table.indexes[indexName]; } } (_e = entity === null || entity === void 0 ? void 0 : entity.setTable) === null || _e === void 0 ? void 0 : _e.call(entity, undefined); } get entities() { return this._entities; } // ----------------------------------------------------------------// // Table actions // ----------------------------------------------------------------// async query(pk, options = {}, params = {}) { var _a; // Generate query parameters with projection data const { payload, EntityProjections, TableProjections } = this.queryParams(pk, options, params, true); // If auto execute enabled if (options.execute || (this.autoExecute && options.execute !== false)) { const result = await this.DocumentClient.send(new lib_dynamodb_1.QueryCommand(payload)); // If auto parse enable if (options.parse || (this.autoParse && options.parse !== false)) { return Object.assign(result, { Items: (_a = result.Items) === null || _a === void 0 ? void 0 : _a.map((item) => { if (typeof item !== 'object' || item === null) { return item; } const itemEntityName = options.parseAsEntity || item[this.Table.entityField !== false ? this.Table.entityField : undefined]; if (typeof itemEntityName !== 'string') { return item; } if (this[itemEntityName]) { return this[itemEntityName].parse(item, EntityProjections[itemEntityName] ? EntityProjections[itemEntityName] : TableProjections ? TableProjections : []); } return item; }), }, // If last evaluated key, return a next function result.LastEvaluatedKey ? { next: () => { return this.query(pk, Object.assign(options, { startKey: result.LastEvaluatedKey }), params); }, } : null); } else { return result; } } else { return payload; } } // Query the table queryParams(pk, options = {}, params = {}, projections = false) { // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions // Deconstruct valid options const { index, limit, reverse, // ScanIndexForward consistent, // ConsistentRead (boolean) capacity, // ReturnConsumedCapacity (none, total, or indexes) select, // Select (all_attributes, all_projected_attributes, specific_attributes, count) eq, // = lt, // < lte, // <= gt, // > gte, // >= between, // between beginsWith, // begins_with, filters, // filter object, attributes, // Projections startKey, entity, // optional entity name to filter aliases parseAsEntity, // optional entity name to parse the result as ..._args // capture extra arguments } = options; // Remove other valid options from options const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x)); // Error on extraneous arguments if (args.length > 0) (0, utils_js_1.error)(`Invalid query options: ${args.join(', ')}`); // Verify pk if ((typeof pk !== 'string' && typeof pk !== 'number') || (typeof pk === 'string' && pk.trim().length === 0)) { (0, utils_js_1.error)(`Query requires a string, number or binary 'partitionKey' as its first parameter`); } // Verify index if (index !== undefined && !this.Table.indexes[index]) { (0, utils_js_1.error)(`'${index}' is not a valid index name`); } // Verify limit if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) { (0, utils_js_1.error)(`'limit' must be a positive integer`); } // Verify reverse if (reverse !== undefined && typeof reverse !== 'boolean') (0, utils_js_1.error)(`'reverse' requires a boolean`); // Verify consistent read if (consistent !== undefined && typeof consistent !== 'boolean') { (0, utils_js_1.error)(`'consistent' requires a boolean`); } // Verify select // TODO: Make dependent on whether or not an index is supplied if (select !== undefined && (typeof select !== 'string' || !['ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', 'COUNT'].includes(select.toUpperCase()))) { (0, utils_js_1.error)(`'select' must be one of 'ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', OR 'COUNT'`); } // Verify entity if (entity !== undefined && (typeof entity !== 'string' || !(entity in this))) { (0, utils_js_1.error)(`'entity' must be a string and a valid table Entity name`); } // Verify capacity if (capacity !== undefined && (typeof capacity !== 'string' || !['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) { (0, utils_js_1.error)(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`); } // Verify startKey // TODO: validate startKey shape if (startKey && (typeof startKey !== 'object' || Array.isArray(startKey))) { (0, utils_js_1.error)(`'startKey' requires a valid object`); } // Default names and values let ExpressionAttributeNames = { '#pk': (index && this.Table.indexes[index].partitionKey) || this.Table.partitionKey, }; let ExpressionAttributeValues = { ':pk': pk }; let KeyConditionExpression = '#pk = :pk'; let FilterExpression; let ProjectionExpression; let EntityProjections = {}; let TableProjections; // FIXME: removed default // Parse sortKey condition operator and value let operator, value, f = ''; if (eq !== undefined) { value = eq; f = 'eq'; operator = '='; } if (lt !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : lt; f = 'lt'; operator = '<'; } if (lte !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : lte; f = 'lte'; operator = '<='; } if (gt !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : gt; f = 'gt'; operator = '>'; } if (gte !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : gte; f = 'gte'; operator = '>='; } if (beginsWith !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : beginsWith; f = 'beginsWith'; operator = 'BEGINS_WITH'; } if (between !== undefined) { value = value ? (0, utils_js_1.conditionError)(f) : between; f = 'between'; operator = 'BETWEEN'; } // If a sortKey condition was set if (operator) { // Get sortKey configuration const sk = index ? this.Table.indexes[index].sortKey ? this.Table.attributes[this.Table.indexes[index].sortKey] || { type: 'string' } : (0, utils_js_1.error)(`Conditional expressions require the index to have a sortKey`) : this.Table.sortKey ? this.Table.attributes[this.Table.sortKey] : (0, utils_js_1.error)(`Conditional expressions require the table to have a sortKey`); // Init validateType const validateType = (0, validateTypes_js_1.default)(); // Add the sortKey attribute name ExpressionAttributeNames['#sk'] = (index && this.Table.indexes[index].sortKey) || this.Table.sortKey; // If between operation if (operator === 'BETWEEN') { // Verify array input if (!Array.isArray(value) || value.length !== 2) { (0, utils_js_1.error)(`'between' conditions requires an array with two values.`); } // Add values and special key condition ExpressionAttributeValues[':sk0'] = validateType(sk, f + '[0]', value[0]); ExpressionAttributeValues[':sk1'] = validateType(sk, f + '[1]', value[1]); KeyConditionExpression += ' and #sk between :sk0 and :sk1'; } else { // Add value ExpressionAttributeValues[':sk'] = validateType(sk, f, value); // If begins_with, add special key condition if (operator === 'BEGINS_WITH') { KeyConditionExpression += ' and begins_with(#sk,:sk)'; } else { KeyConditionExpression += ` and #sk ${operator} :sk`; } } } // If filter expressions if (filters) { // Parse the filter const { expression, names, values } = (0, expressionBuilder_js_1.default)(filters, this, entity); if (Object.keys(names).length > 0) { // TODO: alias attribute field names // console.log(names) // Merge names and values and add filter expression ExpressionAttributeNames = Object.assign(ExpressionAttributeNames, names); ExpressionAttributeValues = Object.assign(ExpressionAttributeValues, values); FilterExpression = expression; } } // If projections if (attributes) { const { names, projections, entities, tableAttrs } = (0, projectionBuilder_js_1.default)(attributes, this, entity, true); if (Object.keys(names).length > 0) { // Merge names and add projection expression ExpressionAttributeNames = Object.assign(ExpressionAttributeNames, names); ProjectionExpression = projections; EntityProjections = entities; TableProjections = tableAttrs; } } // Generate the payload const payload = Object.assign({ TableName: this.name, KeyConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues, }, FilterExpression ? { FilterExpression } : null, ProjectionExpression ? { ProjectionExpression } : null, index ? { IndexName: index } : null, limit ? { Limit: limit } : null, reverse ? { ScanIndexForward: !reverse } : null, consistent ? { ConsistentRead: consistent } : null, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, select ? { Select: select.toUpperCase() } : null, startKey ? { ExclusiveStartKey: startKey } : null, typeof params === 'object' ? params : null); return projections ? { payload, EntityProjections, TableProjections } : payload; } async scan(options = {}, params = {}) { var _a; // Generate query parameters with meta data const { payload, EntityProjections, TableProjections } = this.scanParams(options, params, true); // If auto execute enabled if (options.execute || (this.autoExecute && options.execute !== false)) { const result = await this.DocumentClient.send(new lib_dynamodb_1.ScanCommand(payload)); // If auto parse enable if (options.parse || (this.autoParse && options.parse !== false)) { return Object.assign(result, { Items: (_a = result.Items) === null || _a === void 0 ? void 0 : _a.map(item => { const itemEntityName = options.parseAsEntity || item[this.Table.entityField !== false ? this.Table.entityField : undefined]; const itemEntityInstance = this[itemEntityName]; if (itemEntityInstance != null) { return itemEntityInstance.parse(item, EntityProjections[itemEntityName] ? EntityProjections[itemEntityName] : TableProjections ? TableProjections : []); } else { return item; } }), }, // If last evaluated key, return a next function result.LastEvaluatedKey ? { next: () => { return this.scan(Object.assign(options, { startKey: result.LastEvaluatedKey }), params); }, } : null); } else { return result; } } else { return payload; } } // Generate SCAN Parameters scanParams(options = {}, params = {}, meta = false) { // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions // Deconstruct valid options const { index, limit, consistent, // ConsistentRead (boolean) capacity, // ReturnConsumedCapacity (none, total, or indexes) select, // Select (all_attributes, all_projected_attributes, specific_attributes, count) filters, // filter object, attributes, // Projections segments, // Segments, segment, // Segment startKey, entity, // optional entity name to filter aliases parseAsEntity, // optional entity name to parse the result as ..._args // capture extra arguments } = options; // Remove other valid options from options const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x)); // Error on extraneous arguments if (args.length > 0) (0, utils_js_1.error)(`Invalid scan options: ${args.join(', ')}`); // Verify index if (index !== undefined && !this.Table.indexes[index]) { (0, utils_js_1.error)(`'${index}' is not a valid index name`); } // Verify limit if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) { (0, utils_js_1.error)(`'limit' must be a positive integer`); } // Verify consistent read if (consistent !== undefined && typeof consistent !== 'boolean') { (0, utils_js_1.error)(`'consistent' requires a boolean`); } // Verify select // TODO: Make dependent on whether or not an index is supplied if (select !== undefined && (typeof select !== 'string' || !['ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', 'COUNT'].includes(select.toUpperCase()))) { (0, utils_js_1.error)(`'select' must be one of 'ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', OR 'COUNT'`); } // Verify entity if (entity !== undefined && (typeof entity !== 'string' || !(entity in this))) { (0, utils_js_1.error)(`'entity' must be a string and a valid table Entity name`); } // Verify capacity if (capacity !== undefined && (typeof capacity !== 'string' || !['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) { (0, utils_js_1.error)(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`); } // Verify startKey // TODO: validate startKey shape if (startKey && (typeof startKey !== 'object' || Array.isArray(startKey))) { (0, utils_js_1.error)(`'startKey' requires a valid object`); } // Verify consistent segments if (segments !== undefined && (!Number.isInteger(segments) || segments < 1)) { (0, utils_js_1.error)(`'segments' must be an integer greater than 1`); } if (segment !== undefined && (!Number.isInteger(segment) || segment < 0 || segment >= segments)) { (0, utils_js_1.error)(`'segment' must be an integer greater than or equal to 0 and less than the total number of segments`); } if ((segments !== undefined && segment === undefined) || (segments === undefined && segment !== undefined)) { (0, utils_js_1.error)(`Both 'segments' and 'segment' must be provided`); } // Default names and values let ExpressionAttributeNames = {}; let ExpressionAttributeValues = {}; let FilterExpression; // init FilterExpression let ProjectionExpression; // init ProjectionExpression let EntityProjections = {}; let TableProjections; // If filter expressions if (filters) { // Parse the filter const { expression, names, values } = (0, expressionBuilder_js_1.default)(filters, this, entity); if (Object.keys(names).length > 0) { // TODO: alias attribute field names // console.log(names) // Merge names and values and add filter expression ExpressionAttributeNames = Object.assign(ExpressionAttributeNames, names); ExpressionAttributeValues = Object.assign(ExpressionAttributeValues, values); FilterExpression = expression; } } // If projections if (attributes) { const { names, projections, entities, tableAttrs } = (0, projectionBuilder_js_1.default)(attributes, this, entity, true); if (Object.keys(names).length > 0) { // Merge names and add projection expression ExpressionAttributeNames = Object.assign(ExpressionAttributeNames, names); ProjectionExpression = projections; EntityProjections = entities; TableProjections = tableAttrs; } } // Generate the payload const payload = Object.assign({ TableName: this.name, }, Object.keys(ExpressionAttributeNames).length ? { ExpressionAttributeNames } : null, Object.keys(ExpressionAttributeValues).length ? { ExpressionAttributeValues } : null, FilterExpression ? { FilterExpression } : null, ProjectionExpression ? { ProjectionExpression } : null, index ? { IndexName: index } : null, segments ? { TotalSegments: segments } : null, Number.isInteger(segment) ? { Segment: segment } : null, limit ? { Limit: limit } : null, consistent ? { ConsistentRead: consistent } : null, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, select ? { Select: select.toUpperCase() } : null, startKey ? { ExclusiveStartKey: startKey } : null, typeof params === 'object' ? params : null); return meta ? { payload, EntityProjections, TableProjections } : payload; } // BatchGet Items async batchGet(items, options = {}, params = {}) { // Generate the payload with meta information const { payload, // batchGet payload Tables, // table reference EntityProjections, TableProjections, } = this.batchGetParams(items, options, params, true); const shouldExecute = options.execute || (this.autoExecute && options.execute !== false); if (!shouldExecute) { return payload; } const result = await this.DocumentClient.send(new lib_dynamodb_1.BatchGetCommand(payload)); const shouldParse = options.parse || (this.autoParse && options.parse !== false); if (!shouldParse) { return result; } return this.parseBatchGetResponse(result, Tables, EntityProjections, TableProjections, options); } parseBatchGetResponse(result, // 💥 Retype as Record<string, Table> with inferred type Tables, EntityProjections, TableProjections, options = {}) { return Object.assign(result, // If reponses exist result.Responses ? { // Loop through the tables Responses: Object.keys(result.Responses).reduce((acc, table) => { // Merge in tables return Object.assign(acc, { // Map over the items [(Tables[table] && Tables[table].alias) || table]: result.Responses[table].map((item) => { // Check that the table has a reference, the entityField exists, and that the entity type exists on // the table if (Tables[table] && Tables[table][item[String(Tables[table].Table.entityField)]]) { // Parse the item and pass in projection references return Tables[table][item[String(Tables[table].Table.entityField)]].parse(item, EntityProjections[table] && EntityProjections[table][item[String(Tables[table].Table.entityField)]] ? EntityProjections[table][item[String(Tables[table].Table.entityField)]] : TableProjections[table] ? TableProjections[table] : []); // Else, just return the original item } else { return item; } }), }); }, {}), } : null, // If UnprocessedKeys, return a next function result.UnprocessedKeys && Object.keys(result.UnprocessedKeys).length > 0 ? { next: async () => { const nextResult = await this.DocumentClient.send(new lib_dynamodb_1.BatchGetCommand(Object.assign({ RequestItems: result.UnprocessedKeys }, options.capacity ? { ReturnConsumedCapacity: options.capacity.toUpperCase() } : null))); return this.parseBatchGetResponse(nextResult, Tables, EntityProjections, TableProjections, options); }, } : { next: () => false }); } // Generate BatchGet Params batchGetParams(_items, options = {}, params = {}, meta = false) { var _a; const items = Array.isArray(_items) ? _items : [_items]; // Error on no items if (items.length === 0) (0, utils_js_1.error)(`No items supplied`); const { capacity, consistent, attributes, ..._args } = options; // Remove other valid options from options const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x)); // Error on extraneous arguments if (args.length > 0) (0, utils_js_1.error)(`Invalid batchGet options: ${args.join(', ')}`); // Verify capacity if (capacity !== undefined && (typeof capacity !== 'string' || !['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) { (0, utils_js_1.error)(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`); } // Init RequestItems and Tables reference const RequestItems = {}; const Tables = {}; const TableAliases = {}; const EntityProjections = {}; const TableProjections = {}; // // Loop through items for (const i in items) { const item = items[i]; // Check item for Table reference and key if (item && item.Table && item.Table.Table && item.Key && ((_a = item.Key) === null || _a === void 0 ? void 0 : _a.constructor) === Object) { // Set the table const table = item.Table.name; // If it doesn't exist if (!RequestItems[table]) { // Create a table property with an empty array RequestItems[table] = { Keys: [] }; // Add the table/table alias reference Tables[table] = item.Table; if (item.Table.alias) TableAliases[item.Table.alias] = table; } RequestItems[table].Keys.push(item.Key); } else { (0, utils_js_1.error)(`Item references must contain a valid Table object and Key`); } } // Parse 'consistent' option if (consistent) { // If true, add to all table mappings if (consistent === true) { for (const tbl in RequestItems) { RequestItems[tbl].ConsistentRead = true; } } else if ((consistent === null || consistent === void 0 ? void 0 : consistent.constructor) === Object) { for (const tbl in consistent) { const tbl_name = TableAliases[tbl] || tbl; if (RequestItems[tbl_name]) { if (typeof consistent[tbl] === 'boolean') { RequestItems[tbl_name].ConsistentRead = consistent[tbl]; } else { (0, utils_js_1.error)(`'consistent' values must be booleans (${tbl})`); } } else { (0, utils_js_1.error)(`There are no items for the table or table alias: ${tbl}`); } } } else { (0, utils_js_1.error)(`'consistent' must be a boolean or an map of table names`); } } // If projections if (attributes) { let attrs = attributes; // If an Array, ensure single table and convert to standard format if (Array.isArray(attributes)) { if (Object.keys(RequestItems).length === 1) { attrs = { [Object.keys(RequestItems)[0]]: attributes }; } else { (0, utils_js_1.error)(`'attributes' must use a table map when requesting items from multiple tables`); } } for (const tbl in attrs) { const tbl_name = TableAliases[tbl] || tbl; if (Tables[tbl_name]) { const { names, projections, entities, tableAttrs } = (0, projectionBuilder_js_1.default)(attrs[tbl], Tables[tbl_name], null, true); RequestItems[tbl_name].ExpressionAttributeNames = names; RequestItems[tbl_name].ProjectionExpression = projections; EntityProjections[tbl_name] = entities; TableProjections[tbl_name] = tableAttrs; } else { (0, utils_js_1.error)(`There are no items for the table: ${tbl}`); } } } const payload = Object.assign({ RequestItems }, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, typeof params === 'object' ? params : null); return meta ? { payload, Tables, EntityProjections, TableProjections, } : payload; } // batchGetParams // BatchWrite Items async batchWrite(items, options = {}, params = {}) { // Generate the payload with meta information const payload = this.batchWriteParams(items, options, params); if (options.execute || (this.autoExecute && options.execute !== false)) { const result = await this.DocumentClient.send(new lib_dynamodb_1.BatchWriteCommand(payload)); if (options.parse || (this.autoParse && options.parse !== false)) { return this.parseBatchWriteResponse(result, options); } else { return result; } } else { return payload; } } parseBatchWriteResponse(result, options = {}) { return Object.assign(result, // If UnprocessedItems, return a next function result.UnprocessedItems && Object.keys(result.UnprocessedItems).length > 0 ? { next: async () => { const nextResult = await this.DocumentClient.send(new lib_dynamodb_1.BatchWriteCommand(Object.assign({ RequestItems: result.UnprocessedItems }, options.capacity ? { ReturnConsumedCapacity: options.capacity.toUpperCase() } : null, options.metrics ? { ReturnItemCollectionMetrics: options.metrics.toUpperCase() } : null))); return this.parseBatchWriteResponse(nextResult, options); }, } : { next: () => false }); } /** * Generates parameters for a batchWrite * @param {object} _items - An array of objects generated from putBatch and/or deleteBatch entity calls. * @param {object} [options] - Additional batchWrite options * @param {object} [params] - Additional DynamoDB parameters you wish to pass to the batchWrite request. * @param {boolean} [meta] - Internal flag to enable entity parsing * */ batchWriteParams(_items, options = {}, params = {}, meta = false) { // Convert items to array const items = (Array.isArray(_items) ? _items : [_items]).filter(x => x); // Error on no items if (items.length === 0) (0, utils_js_1.error)(`No items supplied`); const { capacity, metrics, ..._args } = options; // Remove other valid options from options const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x)); // Error on extraneous arguments if (args.length > 0) (0, utils_js_1.error)(`Invalid batchWrite options: ${args.join(', ')}`); // Verify capacity if (capacity !== undefined && (typeof capacity !== 'string' || !['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) { (0, utils_js_1.error)(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`); } // Verify metrics if (metrics !== undefined && (typeof metrics !== 'string' || !['NONE', 'SIZE'].includes(metrics.toUpperCase()))) { (0, utils_js_1.error)(`'metrics' must be one of 'NONE' OR 'SIZE'`); } // Init RequestItems const RequestItems = {}; // Loop through items for (const i in items) { const item = items[i]; const table = Object.keys(item)[0]; // Create a table property with an empty array if it doesn't exist if (!RequestItems[table]) RequestItems[table] = []; // TODO: Add some validation here? // Push request onto the table array RequestItems[table].push(item[table]); } const payload = Object.assign({ RequestItems }, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, metrics ? { ReturnItemCollectionMetrics: metrics.toUpperCase() } : null, typeof params === 'object' ? params : null); const Tables = {}; return meta ? { payload, Tables } : payload; } // batchWriteParams /** * Performs a transactGet operation * @param {object} items - An array of objects generated from getTransaction entity calls. * @param {object} [options] - Additional transactGet options * */ async transactGet(items = [], options = {}) { // Generate the payload with meta information const { payload, Entities } = this.transactGetParams(items, options, true); // If auto execute enabled if (options.execute || (this.autoExecute && options.execute !== false)) { const result = await this.DocumentClient.send(new lib_dynamodb_1.TransactGetCommand(payload)); if (options.parse || (this.autoParse && options.parse !== false)) { // Parse the items using the appropriate entity return Object.assign(result, result.Responses ? { Responses: result.Responses.map((res, i) => { if (res.Item) { return { Item: Entities[i].parse ? Entities[i].parse(res.Item) : res.Item }; } else { return {}; } }), } : null); } else { return result; } } else { return payload; } } transactGetParams(_items, options = {}, meta = false) { const items = Array.isArray(_items) ? _items : _items ? [_items] : []; // Error on no items if (items.length === 0) (0, utils_js_1.error)(`No items supplied`); // Extract valid options const { capacity, // ReturnConsumedCapacity (none, total, or indexes) ..._args } = options; // Remove other valid options from options const args = Object.keys