UNPKG

electrodb

Version:

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

416 lines (381 loc) 11.5 kB
const lib = require("@aws-sdk/lib-dynamodb"); const util = require("@aws-sdk/util-dynamodb"); const { isFunction } = require("./validations"); const { ElectroError, ErrorCodes } = require("./errors"); const { EntityIdentifiers } = require("./types"); const DocumentClientVersions = { v2: "v2", v3: "v3", electro: "electro", }; const unmarshallItem = (value) => { const unmarshall = util.unmarshall || ((val) => val); try { value.Item = unmarshall(value.Item); } catch (err) { console.error("Internal Error: Failed to unmarshal input", err); } return value; }; const v3Methods = ["send"]; const v2Methods = [ "get", "put", "update", "delete", "batchWrite", "batchGet", "scan", "query", "createSet", "transactWrite", "transactGet", ]; const supportedClientVersions = { [DocumentClientVersions.v2]: v2Methods, [DocumentClientVersions.v3]: v3Methods, }; class DocumentClientV2Wrapper { static init(client) { return new DocumentClientV2Wrapper(client, lib); } constructor(client, lib) { this.client = client; this.lib = lib; this.__v = "v2"; } _wrapRequest(makeRequest, signal) { return { promise: () => { return new Promise((resolve, reject) => { if (signal && signal.aborted) { return reject( new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ), ); } const request = makeRequest(); const onAbort = () => { request.abort(); reject( new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ), ); }; if (signal) { signal.addEventListener("abort", onAbort, { once: true }); } request .promise() .then((result) => { if (signal) { signal.removeEventListener("abort", onAbort); } resolve(result); }) .catch((err) => { if (signal) { signal.removeEventListener("abort", onAbort); } reject(err); }); }); }, }; } get(params, options = {}) { return this._wrapRequest(() => this.client.get(params), options.abortSignal); } put(params, options = {}) { return this._wrapRequest(() => this.client.put(params), options.abortSignal); } update(params, options = {}) { return this._wrapRequest(() => this.client.update(params), options.abortSignal); } delete(params, options = {}) { return this._wrapRequest(() => this.client.delete(params), options.abortSignal); } batchWrite(params, options = {}) { return this._wrapRequest(() => this.client.batchWrite(params), options.abortSignal); } batchGet(params, options = {}) { return this._wrapRequest(() => this.client.batchGet(params), options.abortSignal); } scan(params, options = {}) { return this._wrapRequest(() => this.client.scan(params), options.abortSignal); } query(params, options = {}) { return this._wrapRequest(() => this.client.query(params), options.abortSignal); } _transact(makeTransactionRequest, signal) { return { promise: () => { return new Promise((resolve, reject) => { if (signal && signal.aborted) { return reject( new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ), ); } const transactionRequest = makeTransactionRequest(); let cancellationReasons; transactionRequest.on("extractError", (response) => { try { cancellationReasons = JSON.parse( response.httpResponse.body.toString(), ).CancellationReasons; } catch (err) {} }); const onAbort = () => { transactionRequest.abort(); reject( new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ), ); }; if (signal) { signal.addEventListener("abort", onAbort, { once: true }); } transactionRequest .promise() .then((result) => { if (signal) { signal.removeEventListener("abort", onAbort); } resolve(result); }) .catch((err) => { if (signal) { signal.removeEventListener("abort", onAbort); } if (err) { if (Array.isArray(cancellationReasons)) { resolve({ canceled: cancellationReasons.map((reason) => { if (reason.Item) { return unmarshallItem(reason); } return reason; }), }); } else { reject(err); } } }); }); }, }; } transactWrite(params, options = {}) { return this._transact(() => this.client.transactWrite(params), options.abortSignal); } transactGet(params, options = {}) { return this._transact(() => this.client.transactGet(params), options.abortSignal); } createSet(value, ...rest) { if (Array.isArray(value)) { return this.client.createSet(value, ...rest); } else { return this.client.createSet([value], ...rest); } } } class DocumentClientV3Wrapper { static init(client) { return new DocumentClientV3Wrapper(client, lib); } constructor(client, lib) { this.client = client; this.lib = lib; this.__v = "v3"; } promiseWrap(fn, signal) { return { promise: async () => { if (signal && signal.aborted) { throw new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ); } try { return await fn(); } catch (err) { if (signal && signal.aborted) { throw new ElectroError( ErrorCodes.OperationAborted, "The operation was aborted", ); } throw err; } }, }; } get(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.GetCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } put(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.PutCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } update(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.UpdateCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } delete(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.DeleteCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } batchWrite(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.BatchWriteCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } batchGet(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.BatchGetCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } scan(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.ScanCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } query(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.QueryCommand(params); return this.client.send(command, { abortSignal: options.abortSignal }); }, options.abortSignal); } transactWrite(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.TransactWriteCommand(params); return this.client .send(command, { abortSignal: options.abortSignal }) .then((result) => { return result; }) .catch((err) => { if (err.CancellationReasons) { return { canceled: err.CancellationReasons.map((reason) => { if (reason.Item) { return unmarshallItem(reason); } return reason; }), }; } throw err; }); }, options.abortSignal); } transactGet(params, options = {}) { return this.promiseWrap(() => { const command = new this.lib.TransactGetCommand(params); return this.client .send(command, { abortSignal: options.abortSignal }) .then((result) => { return result; }) .catch((err) => { if (err.CancellationReasons) { return { canceled: err.CancellationReasons.map((reason) => { if (reason.Item) { return unmarshallItem(reason); } return reason; }), }; } throw err; }); }, options.abortSignal); } createSet(value) { if (Array.isArray(value)) { return new Set(value); } else { return new Set([value]); } } } function identifyClientVersion(client = {}) { if ( client instanceof DocumentClientV3Wrapper || client instanceof DocumentClientV2Wrapper ) { return DocumentClientVersions.electro; } for (const [version, methods] of Object.entries(supportedClientVersions)) { const hasMethods = methods.every((method) => { return method in client && isFunction(client[method]); }); if (hasMethods) { return version; } } } function normalizeClient(client) { if (client === undefined) return client; const version = identifyClientVersion(client); switch (version) { case DocumentClientVersions.v3: return DocumentClientV3Wrapper.init(client); case DocumentClientVersions.v2: return DocumentClientV2Wrapper.init(client); case DocumentClientVersions.electro: return client; default: throw new ElectroError( ErrorCodes.InvalidClientProvided, "Invalid DynamoDB Document Client provided. ElectroDB supports the v2 and v3 DynamoDB Document Clients from the aws-sdk", ); } } function normalizeConfig(config = {}) { const identifiers = config.identifiers || {}; return { ...config, client: normalizeClient(config.client), identifiers: { entity: identifiers.entity || EntityIdentifiers.entity, version: identifiers.version || EntityIdentifiers.version } }; } module.exports = { util, v2Methods, v3Methods, normalizeClient, normalizeConfig, identifyClientVersion, DocumentClientVersions, supportedClientVersions, DocumentClientV3Wrapper, DocumentClientV2Wrapper, };