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
JavaScript
"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),
)}.`,