UNPKG

electrodb-temp

Version:

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

392 lines (360 loc) 11.9 kB
const { AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes, } = require("./types"); const { UpdateOperations } = require("./updateOperations"); const { FilterOperations } = require("./filterOperations"); const e = require("./errors"); const u = require("./util"); class ExpressionState { constructor({ prefix } = {}) { this.names = {}; this.values = {}; this.paths = {}; this.counts = {}; this.impacted = {}; this.expression = ""; this.prefix = prefix || ""; this.refs = {}; } incrementName(name) { if (this.counts[name] === undefined) { this.counts[name] = 0; } return `${this.prefix}${this.counts[name]++}`; } formatName(name = "") { const nameWasNotANumber = isNaN(name); name = `${name}`.replaceAll(/[^\w]/g, ""); if (name.length === 0) { name = "p"; } else if (nameWasNotANumber !== isNaN(name)) { // name became number due to replace name = `p${name}`; } return name; } // todo: make the structure: name, value, paths setName(paths, name, value) { name = this.formatName(name); let json = ""; let expression = ""; const prop = `#${name}`; if (Object.keys(paths).length === 0) { json = `${name}`; expression = `${prop}`; this.names[prop] = value; } else if (isNaN(name)) { json = `${paths.json}.${name}`; expression = `${paths.expression}.${prop}`; this.names[prop] = value; } else { json = `${paths.json}[${name}]`; expression = `${paths.expression}[${name}]`; } return { json, expression, prop }; } getNames() { return this.names; } setValue(name, value) { name = this.formatName(name); let valueCount = this.incrementName(name); let expression = `:${name}${valueCount}`; this.values[expression] = value; return expression; } updateValue(name, value) { this.values[name] = value; } getValues() { return this.values; } setPath(path, value) { this.paths[path] = value; } setExpression(expression) { this.expression = expression; } getExpression() { return this.expression; } setImpacted(operation, path, ref) { this.impacted[path] = operation; this.refs[path] = ref; } } class AttributeOperationProxy { constructor({ builder, attributes = {}, operations = {} }) { this.ref = { attributes, operations, }; this.attributes = AttributeOperationProxy.buildAttributes( builder, attributes, ); this.operations = AttributeOperationProxy.buildOperations( builder, operations, ); } invokeCallback(op, ...params) { return op(this.attributes, this.operations, ...params); } performOperation({ operation, path, value, force = false }) { if (value === undefined) { return; } const parts = u.parseJSONPath(path); let attribute = this.attributes; for (let part of parts) { attribute = attribute[part]; } if (attribute) { this.operations[operation](attribute, value); const { target } = attribute(); if (target.readOnly && !force) { throw new Error( `Attribute "${target.path}" is Read-Only and cannot be updated`, ); } } } fromObject(operation, record) { for (let path of Object.keys(record)) { this.performOperation({ operation, path, value: record[path], }); } } fromArray(operation, paths) { for (let path of paths) { const parts = u.parseJSONPath(path); let attribute = this.attributes; for (let part of parts) { attribute = attribute[part]; } if (attribute) { this.operations[operation](attribute); const { target } = attribute(); if (target.readOnly) { throw new Error( `Attribute "${target.path}" is Read-Only and cannot be updated`, ); } else if (operation === ItemOperations.remove && target.required) { throw new Error( `Attribute "${target.path}" is Required and cannot be removed`, ); } } } } static buildOperations(builder, operations) { let ops = {}; let seen = new Map(); for (let operation of Object.keys(operations)) { let { template, canNest, rawValue, rawField } = operations[operation]; Object.defineProperty(ops, operation, { get: () => { return (property, ...values) => { if (property === undefined) { throw new e.ElectroError( e.ErrorCodes.InvalidWhere, `Invalid/Unknown property passed in where clause passed to operation: '${operation}'`, ); } if (property[AttributeProxySymbol]) { const { commit, target } = property(); const fixedValues = values .map((value) => target.applyFixings(value)) .filter((value) => value !== undefined); const isFilterBuilder = builder.type === BuilderTypes.filter; const takesValueArgument = template.length > 3; const isAcceptableValue = fixedValues.every((value) => { const seenAttributes = seen.get(value); if (seenAttributes) { return seenAttributes.every((v) => target.acceptable(v)); } return target.acceptable(value); }); const shouldCommit = // if it is a filterBuilder than we don't care what they pass because the user needs more freedom here isFilterBuilder || // if the operation does not take a value argument then not committing here could cause problems. // this should be revisited to make more robust, we could hypothetically store the commit in the // "seen" map for when the value is used, but that's a lot of new complexity !takesValueArgument || // if the operation takes a value, we should determine if that value is acceptable. For // example, in the cases of a "set" we check to see if it is empty, or if the value is // undefined, we should not commit. The "fixedValues" length check is because the // "fixedValues" array has been filtered for undefined, so no length there indicates an // undefined value was passed. (takesValueArgument && isAcceptableValue && fixedValues.length > 0); if (!shouldCommit) { return ""; } const paths = commit(); const attributeValues = []; let hasNestedValue = false; for (let fixedValue of fixedValues) { if (seen.has(fixedValue)) { attributeValues.push(fixedValue); hasNestedValue = true; } else { let attributeValueName = builder.setValue( target.name, fixedValue, ); builder.setPath(paths.json, { value: fixedValue, name: attributeValueName, }); attributeValues.push(attributeValueName); } } const options = { nestedValue: hasNestedValue, createValue: (name, value) => builder.setValue(`${target.name}_${name}`, value), }; const formatted = template( options, target, paths.expression, ...attributeValues, ); builder.setImpacted(operation, paths.json, target); if (canNest) { seen.set(paths.expression, attributeValues); seen.set(formatted, attributeValues); } if ( builder.type === BuilderTypes.update && formatted && typeof formatted.operation === "string" && typeof formatted.expression === "string" ) { builder.add(formatted.operation, formatted.expression); return formatted.expression; } return formatted; } else if (rawValue) { // const {json, expression} = builder.setName({}, property, property); let attributeValueName = builder.setValue(property, property); builder.setPath(property, { value: property, name: attributeValueName, }); const formatted = template({}, attributeValueName); seen.set(attributeValueName, [property]); seen.set(formatted, [property]); return formatted; } else if (rawField) { const { prop, expression } = builder.setName( {}, property, property, ); const formatted = template({}, null, prop); seen.set(expression, [property]); seen.set(formatted, [property]); return formatted; } else { throw new e.ElectroError( e.ErrorCodes.InvalidWhere, `Invalid Attribute in where clause passed to operation '${operation}'. Use injected attributes only.`, ); } }; }, }); } return ops; } static pathProxy(build) { return new Proxy(() => build(), { get: (_, prop, o) => { if (prop === AttributeProxySymbol) { return true; } else { return AttributeOperationProxy.pathProxy(() => { const { commit, root, target, builder } = build(); const attribute = target.getChild(prop); const nestedAny = attribute.type === AttributeTypes.any && // if the name doesn't match that's because we are nested under 'any' attribute.name !== prop; let field; if (attribute === undefined) { throw new Error( `Invalid attribute "${prop}" at path "${target.path}.${prop}"`, ); } else if (nestedAny) { field = prop; } else { field = attribute.field; } return { root, builder, nestedAny, target: attribute, commit: () => { const paths = commit(); return builder.setName(paths, prop, field); }, }; }); } }, }); } static buildAttributes(builder, attributes) { let attr = {}; for (let [name, attribute] of Object.entries(attributes)) { Object.defineProperty(attr, name, { get: () => { return AttributeOperationProxy.pathProxy(() => { return { root: attribute, target: attribute, builder, commit: () => builder.setName({}, attribute.name, attribute.field), }; }); }, }); } return attr; } } const FilterOperationNames = Object.keys(FilterOperations).reduce( (ops, name) => { ops[name] = name; return ops; }, {}, ); const UpdateOperationNames = Object.keys(UpdateOperations).reduce( (ops, name) => { ops[name] = name; return ops; }, {}, ); module.exports = { UpdateOperations, UpdateOperationNames, FilterOperations, FilterOperationNames, ExpressionState, AttributeOperationProxy, };