UNPKG

@barchart/common-node-js

Version:

Common classes, utilities, and functions for building Node.js servers

304 lines (251 loc) 7.94 kB
const is = require('@barchart/common-js/lang/is'), object = require('@barchart/common-js/lang/object'); const Action = require('./Action'), Filter = require('./Filter'), Index = require('./../../schema/definitions/Index'), KeyType = require('./../../schema/definitions/KeyType'), OrderingType = require('./OrderingType'), Table = require('./../../schema/definitions/Table'); module.exports = (() => { 'use strict'; /** * The definition of a table (or index) query. * * @public * @extends {Action} * @param {Table} table * @param {Index} index * @param {Filter} keyFilter * @param {Filter} resultsFilter * @param {Array<Attribute>} attributes * @param {OrderingType=} orderingType * @param {Boolean=} consistentRead * @param {Boolean=} skipDeserialization * @param {Boolean=} countOnly * @param {String=} description * @param {Boolean=} monitorCapacityConsumed */ class Query extends Action { constructor(table, index, keyFilter, resultsFilter, parallelFilter, attributes, limit, orderingType, consistentRead, skipDeserialization, countOnly, description, monitorCapacityConsumed) { super(table, index, (description || '[Unnamed Query]')); this._keyFilter = keyFilter || null; this._resultsFilter = resultsFilter || null; this._parallelFilter = parallelFilter || null; this._attributes = attributes || [ ]; this._limit = limit || null; this._consistentRead = consistentRead || false; this._skipDeserialization = skipDeserialization || false; this._countOnly = countOnly || false; this._monitorCapacityConsumed = monitorCapacityConsumed || false; this._orderingType = orderingType || OrderingType.ASCENDING; } /** * A {@link Filter} to apply to key of the table (or index). * * @public * @returns {Filter} */ get keyFilter() { return this._keyFilter; } /** * A {@link Filter} to apply to results of the query (after the * PartitionTransformer has been applied). * * @public * @returns {Filter} */ get resultsFilter() { return this._resultsFilter; } /** * A {@link Filter} to applied to the range key of the table (which is added to * the existing PartitionTransformer). This filter is used to split the query into a smaller * set -- based on range key. * * @public * @returns {Filter} */ get parallelFilter() { return this._parallelFilter; } /** * The {@link Attribute} instances to select. If the array is empty, all * attributes will be selected. * * @public * @returns {Attribute[]} */ get attributes() { return [...this._attributes]; } /** * The maximum number of results to returns from the query. A null value * will be interpreted as no limit. * * @public * @returns {Number|null} */ get limit() { return this._limit; } /** * The desired order of the results. * * @public * @returns {OrderingType} */ get orderingType() { return this._orderingType; } /** * If true, a consistent read will be used. * * @public * @returns {Boolean} */ get consistentRead() { return this._consistentRead; } /** * If true, the query will return records in DynamoDB format, skipping * the conversion to normal objects. * * @public * @returns {Boolean} */ get skipDeserialization() { return this._skipDeserialization; } /** * If true, the query will return a record count only. * * @public * @returns {Boolean} */ get countOnly() { return this._countOnly; } /** * If true, the total RCU (read capacity units) consumed will be monitored. * * @public * @returns {Boolean} */ get monitorCapacityConsumed() { return this._monitorCapacityConsumed; } /** * Throws an {@link Error} if the instance is invalid. * * @public */ validate() { if (!(this.table instanceof Table)) { throw new Error('Table data type is invalid.'); } if (this.index !== null && !(this.index instanceof Index)) { throw new Error('Index data type is invalid.'); } if (this.index !== null && !this.table.indices.some(i => i.equals(this.index, true))) { throw new Error('The index must belong to the table.'); } if (this._index !== null && this._consistentRead && !this._index.type.allowsConsistentReads) { throw new Error('Unable to apply consistent read to index.'); } if (!(this._keyFilter instanceof Filter)) { throw new Error('The key filter data type is invalid.'); } this._keyFilter.validate(); let keys; if (this.index === null) { keys = this.table.keys; } else { keys = this.index.keys; } if (this._keyFilter.expressions.filter(e => e.attribute.name === (keys.find(k => k.keyType === KeyType.HASH)).attribute.name).length !== 1) { throw new Error('The key filter must reference the hash key.'); } if (this._resultsFilter !== null) { if (!(this._resultsFilter instanceof Filter)) { throw new Error('The results filter data type is invalid.'); } this._resultsFilter.validate(); } if (this._parallelFilter !== null) { if (!(this._parallelFilter instanceof Filter)) { throw new Error('The parallel filter data type is invalid.'); } if (this._parallelFilter.expressions.filter(e => e.attribute.name === (keys.find(k => k.keyType === KeyType.RANGE)).attribute.name).length !== 1) { throw new Error('The key parallel must reference the range key.'); } } if (!(this._orderingType instanceof OrderingType)) { throw new Error('The ordering type is invalid.'); } if (this._limit !== null && (!is.large(this._limit) || !(this._limit > 0))) { throw new Error('The limit must be a positive integer.'); } } /** * Outputs an object suitable for running a "query" operation using * the DynamoDB SDK. * * @public * @returns {Object} */ toQuerySchema() { this.validate(); const schema = { TableName: this.table.name }; if (this.index !== null) { schema.IndexName = this.index.name; } let attributes = this.attributes; if (attributes.length !== 0) { schema.Select = 'SPECIFIC_ATTRIBUTES'; schema.ProjectionExpression = Action.getProjectionExpression(this.table, attributes); } else if (this.countOnly) { schema.Select = 'COUNT'; } let keyFilterToUse; if (this._parallelFilter === null) { keyFilterToUse = this._keyFilter; } else { keyFilterToUse = Filter.merge(this._keyFilter, this._parallelFilter); } const keyExpressionData = Action.getConditionExpressionData(this.table, keyFilterToUse); schema.KeyConditionExpression = keyExpressionData.expression; attributes = attributes.concat(keyFilterToUse.expressions.map(e => e.attribute)); let valueAliases = keyExpressionData.valueAliases; if (this._resultsFilter !== null) { const resultsExpressionData = Action.getConditionExpressionData(this.table, this._resultsFilter, keyExpressionData.offset); schema.FilterExpression = resultsExpressionData.expression; attributes = attributes.concat(this._resultsFilter.expressions.map(e => e.attribute)); valueAliases = object.merge(keyExpressionData.valueAliases, resultsExpressionData.valueAliases); } else { valueAliases = keyExpressionData.valueAliases; } if (attributes.length !== 0) { schema.ExpressionAttributeNames = Action.getExpressionAttributeNames(this._table, attributes); } schema.ExpressionAttributeValues = valueAliases; schema.ScanIndexForward = this._orderingType.forward; if (this._limit !== null) { schema.Limit = this._limit; } if (this._consistentRead) { schema.ConsistentRead = true; } if (this._monitorCapacityConsumed) { schema.ReturnConsumedCapacity = 'TOTAL'; } return schema; } toString() { return '[Query]'; } } return Query; })();