electrodb-temp
Version:
A library to more easily create and interact with multiple entities and heretical relationships in dynamodb
1,687 lines (1,615 loc) • 47.8 kB
JavaScript
const {
QueryTypes,
MethodTypes,
ItemOperations,
ExpressionTypes,
TransactionCommitSymbol,
TransactionOperations,
TerminalOperation,
KeyTypes,
IndexTypes,
UpsertOperations,
ComparisonTypes,
} = require("./types");
const {
AttributeOperationProxy,
UpdateOperations,
FilterOperationNames,
UpdateOperationNames,
} = require("./operations");
const { UpdateExpression } = require("./update");
const { FilterExpression } = require("./where");
const v = require("./validations");
const e = require("./errors");
const u = require("./util");
const methodChildren = {
upsert: [
"upsertSet",
"upsertAppend",
"upsertAdd",
"go",
"params",
"upsertSubtract",
"commit",
"upsertIfNotExists",
"where",
],
update: [
"data",
"set",
"append",
"add",
"updateRemove",
"updateDelete",
"go",
"params",
"subtract",
"commit",
"composite",
"ifNotExists",
"where",
],
put: ["where", "params", "go", "commit"],
del: ["where", "params", "go", "commit"],
};
function batchAction(action, type, entity, state, payload) {
if (state.getError() !== null) {
return state;
}
try {
state.setMethod(type);
for (let facets of payload) {
let batchState = action(entity, state.createSubState(), facets);
if (batchState.getError() !== null) {
throw batchState.getError();
}
}
return state;
} catch (err) {
state.setError(err);
return state;
}
}
let clauses = {
index: {
name: "index",
children: [
"check",
"get",
"delete",
"update",
"query",
"upsert",
"put",
"scan",
"collection",
"clusteredCollection",
"create",
"remove",
"patch",
"batchPut",
"batchDelete",
"batchGet",
],
},
clusteredCollection: {
name: "clusteredCollection",
action(
entity,
state,
collection = "",
facets /* istanbul ignore next */ = {},
) {
if (state.getError() !== null) {
return state;
}
try {
const { pk, sk } = state.getCompositeAttributes();
return state
.setType(QueryTypes.clustered_collection)
.setMethod(MethodTypes.query)
.setCollection(collection)
.setPK(entity._expectFacets(facets, pk))
.ifSK(() => {
const { composites, unused } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
state.setSK(composites);
state.beforeBuildParams(({ options, state }) => {
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
if (
options.compare === ComparisonTypes.attributes ||
options.compare === ComparisonTypes.v2
) {
if (
!entity.model.indexes[accessPattern].sk.isFieldRef &&
sk.length > 1
) {
state.filterProperties(FilterOperationNames.eq, {
...unused,
...composites,
});
}
}
});
})
.whenOptions(({ options, state }) => {
if (!options.ignoreOwnership && !state.getParams()) {
state.query.options.expressions.names = {
...state.query.options.expressions.names,
...state.query.options.identifiers.names,
};
state.query.options.expressions.values = {
...state.query.options.expressions.values,
...state.query.options.identifiers.values,
};
state.query.options.expressions.expression =
state.query.options.expressions.expression.length > 1
? `(${state.query.options.expressions.expression}) AND ${state.query.options.identifiers.expression}`
: `${state.query.options.identifiers.expression}`;
}
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["between", "gte", "gt", "lte", "lt", "begins", "params", "go"],
},
collection: {
name: "collection",
/* istanbul ignore next */
action(
entity,
state,
collection = "",
facets /* istanbul ignore next */ = {},
) {
if (state.getError() !== null) {
return state;
}
try {
const { pk, sk } = state.getCompositeAttributes();
return state
.setType(QueryTypes.collection)
.setMethod(MethodTypes.query)
.setCollection(collection)
.setPK(entity._expectFacets(facets, pk))
.whenOptions(({ options, state }) => {
if (!options.ignoreOwnership && !state.getParams()) {
state.query.options.expressions.names = {
...state.query.options.expressions.names,
...state.query.options.identifiers.names,
};
state.query.options.expressions.values = {
...state.query.options.expressions.values,
...state.query.options.identifiers.values,
};
state.query.options.expressions.expression =
state.query.options.expressions.expression.length > 1
? `(${state.query.options.expressions.expression}) AND ${state.query.options.identifiers.expression}`
: `${state.query.options.identifiers.expression}`;
}
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["params", "go"],
},
scan: {
name: "scan",
action(entity, state, config) {
if (state.getError() !== null) {
return state;
}
try {
return state
.setMethod(MethodTypes.scan)
.whenOptions(({ state, options }) => {
if (!options.ignoreOwnership && !state.getParams()) {
state.unsafeApplyFilter(
{},
FilterOperationNames.eq,
entity.identifiers.entity,
entity.getName(),
);
state.unsafeApplyFilter(
{},
FilterOperationNames.eq,
entity.identifiers.version,
entity.getVersion(),
);
}
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["params", "go"],
},
get: {
name: "get",
/* istanbul ignore next */
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
const { pk, sk } = state.getCompositeAttributes();
const { composites } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
return state
.setMethod(MethodTypes.get)
.setType(QueryTypes.eq)
.setPK(entity._expectFacets(facets, pk))
.ifSK(() => {
entity._expectFacets(facets, sk);
state.setSK(composites);
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["params", "go", "commit"],
},
check: {
name: "check",
action(...params) {
return clauses.get.action(...params).setMethod(MethodTypes.check);
},
children: ["commit"],
},
batchGet: {
name: "batchGet",
action: (entity, state, payload) =>
batchAction(
clauses.get.action,
MethodTypes.batchGet,
entity,
state,
payload,
),
children: ["params", "go"],
},
batchDelete: {
name: "batchDelete",
action: (entity, state, payload) =>
batchAction(
clauses.delete.action,
MethodTypes.batchWrite,
entity,
state,
payload,
),
children: ["params", "go"],
},
delete: {
name: "delete",
/* istanbul ignore next */
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
const { pk, sk } = state.getCompositeAttributes();
const pkComposite = entity._expectFacets(facets, pk);
state.addOption("_includeOnResponseItem", pkComposite);
return state
.setMethod(MethodTypes.delete)
.setType(QueryTypes.eq)
.setPK(pkComposite)
.ifSK(() => {
entity._expectFacets(facets, sk);
const skComposite = state.buildQueryComposites(facets, sk);
state.setSK(skComposite);
state.addOption("_includeOnResponseItem", {
...skComposite,
...pkComposite,
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["where", "params", "go", "commit"],
},
remove: {
name: "remove",
/* istanbul ignore next */
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
const attributes = state.getCompositeAttributes();
const filter = state.query.filter[ExpressionTypes.ConditionExpression];
const { pk, sk } = entity._getPrimaryIndexFieldNames();
filter.unsafeSet({}, FilterOperationNames.exists, pk);
if (sk) {
filter.unsafeSet({}, FilterOperationNames.exists, sk);
}
const pkComposite = entity._expectFacets(facets, attributes.pk);
state.addOption("_includeOnResponseItem", pkComposite);
return state
.setMethod(MethodTypes.delete)
.setType(QueryTypes.eq)
.setPK(pkComposite)
.ifSK(() => {
entity._expectFacets(facets, attributes.sk);
const skComposite = state.buildQueryComposites(
facets,
attributes.sk,
);
state.setSK(skComposite);
state.addOption("_includeOnResponseItem", {
...skComposite,
...pkComposite,
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.del,
},
upsert: {
name: "upsert",
action(entity, state, payload = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state
.setMethod(MethodTypes.upsert)
.setType(QueryTypes.eq)
.applyUpsert(UpsertOperations.set, payload)
.beforeBuildParams(({ state }) => {
const { upsert, update, updateProxy } = state.query;
state.query.update.set(entity.identifiers.entity, entity.getName());
state.query.update.set(
entity.identifiers.version,
entity.getVersion(),
);
// only "set" data is used to make keys
const setData = {};
const nonSetData = {};
let allData = {};
for (const name in upsert.data) {
const { operation, value } = upsert.data[name];
allData[name] = value;
if (operation === UpsertOperations.set) {
setData[name] = value;
} else {
nonSetData[name] = value;
}
}
const upsertData = entity.model.schema.checkCreate({ ...allData });
const attributes = state.getCompositeAttributes();
const pkComposite = entity._expectFacets(upsertData, attributes.pk);
state
.addOption("_includeOnResponseItem", pkComposite)
.setPK(pkComposite)
.ifSK(() => {
entity._expectFacets(upsertData, attributes.sk);
const skComposite = entity._buildQueryFacets(
upsertData,
attributes.sk,
);
state.setSK(skComposite);
state.addOption("_includeOnResponseItem", {
...skComposite,
...pkComposite,
});
});
const appliedData = entity.model.schema.applyAttributeSetters({
...upsertData,
});
const onlySetAppliedData = {};
const nonSetAppliedData = {};
for (const name in appliedData) {
const value = appliedData[name];
const isSetOperation = setData[name] !== undefined;
const cameFromApplyingSetters = allData[name] === undefined;
const isNotUndefined = appliedData[name] !== undefined;
const applyAsSet = isSetOperation || cameFromApplyingSetters;
if (applyAsSet && isNotUndefined) {
onlySetAppliedData[name] = value;
} else {
nonSetAppliedData[name] = value;
}
}
// we build this above, and set them to state, but use it here, not ideal but
// the way it worked out so that this could be wrapped in beforeBuildParams
const { pk } = state.query.keys;
const sk = state.query.keys.sk[0];
const {
updatedKeys,
setAttributes,
indexKey,
deletedKeys = [],
} = entity._getPutKeys(pk, sk && sk.facets, onlySetAppliedData);
for (const deletedKey of deletedKeys) {
state.query.update.remove(deletedKey);
}
// calculated here but needs to be used when building the params
upsert.indexKey = indexKey;
// only "set" data is used to make keys
const setFields = Object.entries(
entity.model.schema.translateToFields(setAttributes),
);
// add the keys impacted except for the table index keys; they are upserted
// automatically by dynamo
for (const key in updatedKeys) {
const value = updatedKeys[key];
if (indexKey[key] === undefined) {
setFields.push([key, value]);
}
}
entity._maybeApplyUpsertUpdate({
fields: setFields,
operation: UpsertOperations.set,
updateProxy,
update,
});
for (const name in nonSetData) {
const value = appliedData[name];
if (value === undefined || upsert.data[name] === undefined) {
continue;
}
const { operation } = upsert.data[name];
const fields = entity.model.schema.translateToFields({
[name]: value,
});
entity._maybeApplyUpsertUpdate({
fields: Object.entries(fields),
updateProxy,
operation,
update,
});
}
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
put: {
name: "put",
/* istanbul ignore next */
action(entity, state, payload = {}) {
if (state.getError() !== null) {
return state;
}
try {
let record = entity.model.schema.checkCreate({ ...payload });
const attributes = state.getCompositeAttributes();
return state
.setMethod(MethodTypes.put)
.setType(QueryTypes.eq)
.applyPut(record)
.setPK(entity._expectFacets(record, attributes.pk))
.ifSK(() => {
entity._expectFacets(record, attributes.sk);
state.setSK(state.buildQueryComposites(record, attributes.sk));
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.put,
},
batchPut: {
name: "batchPut",
action: (entity, state, payload) =>
batchAction(
clauses.put.action,
MethodTypes.batchWrite,
entity,
state,
payload,
),
children: ["params", "go"],
},
create: {
name: "create",
action(entity, state, payload) {
if (state.getError() !== null) {
return state;
}
try {
let record = entity.model.schema.checkCreate({ ...payload });
const attributes = state.getCompositeAttributes();
const filter = state.query.filter[ExpressionTypes.ConditionExpression];
const { pk, sk } = entity._getPrimaryIndexFieldNames();
filter.unsafeSet({}, FilterOperationNames.notExists, pk);
if (sk) {
filter.unsafeSet({}, FilterOperationNames.notExists, sk);
}
return state
.setMethod(MethodTypes.put)
.setType(QueryTypes.eq)
.applyPut(record)
.setPK(entity._expectFacets(record, attributes.pk))
.ifSK(() => {
entity._expectFacets(record, attributes.sk);
state.setSK(state.buildQueryComposites(record, attributes.sk));
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.put,
},
patch: {
name: "patch",
action(entity, state, facets) {
if (state.getError() !== null) {
return state;
}
try {
const attributes = state.getCompositeAttributes();
const filter = state.query.filter[ExpressionTypes.ConditionExpression];
const { pk, sk } = entity._getPrimaryIndexFieldNames();
filter.unsafeSet({}, FilterOperationNames.exists, pk);
if (sk) {
filter.unsafeSet({}, FilterOperationNames.exists, sk);
}
const pkComposite = entity._expectFacets(facets, attributes.pk);
state.addOption("_includeOnResponseItem", pkComposite);
return state
.setMethod(MethodTypes.update)
.setType(QueryTypes.eq)
.setPK(pkComposite)
.ifSK(() => {
entity._expectFacets(facets, attributes.sk);
const skComposite = state.buildQueryComposites(
facets,
attributes.sk,
);
state.setSK(skComposite);
state.addOption("_includeOnResponseItem", {
...skComposite,
...pkComposite,
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
update: {
name: "update",
action(entity, state, facets) {
if (state.getError() !== null) {
return state;
}
try {
const attributes = state.getCompositeAttributes();
const pkComposite = entity._expectFacets(facets, attributes.pk);
state.addOption("_includeOnResponseItem", pkComposite);
return state
.setMethod(MethodTypes.update)
.setType(QueryTypes.eq)
.setPK(pkComposite)
.ifSK(() => {
entity._expectFacets(facets, attributes.sk);
const skComposite = state.buildQueryComposites(
facets,
attributes.sk,
);
state.setSK(skComposite);
state.addOption("_includeOnResponseItem", {
...pkComposite,
...skComposite,
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
data: {
name: "data",
action(entity, state, cb) {
if (state.getError() !== null) {
return state;
}
try {
state.query.updateProxy.invokeCallback(cb);
for (const path of Object.keys(state.query.update.refs)) {
const operation = state.query.update.impacted[path];
const attribute = state.query.update.refs[path];
// note: keyValue will be empty if the user used `name`/`value` operations
// because it becomes hard to know how they are used and which attribute
// should validate the change. This is an edge case however, this change
// still improves on the existing implementation.
const keyValue = state.query.update.paths[path] || {};
if (!attribute) {
throw new e.ElectroAttributeValidationError(
path,
`Attribute "${path}" does not exist on model.`,
);
}
entity.model.schema.checkOperation(
attribute,
operation,
keyValue.value,
);
}
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
set: {
name: "set",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.set, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
upsertSet: {
name: "set",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data, { allowReadOnly: true });
state.query.upsert.addData(UpsertOperations.set, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
composite: {
name: "composite",
action(entity, state, composites = {}) {
if (state.getError() !== null) {
return state;
}
try {
for (const attrName in composites) {
// todo: validate attrName is facet
if (entity.model.facets.byAttr[attrName]) {
const wasSet = state.query.update.addComposite(
attrName,
composites[attrName],
);
if (!wasSet) {
throw new e.ElectroError(
e.ErrorCodes.DuplicateUpdateCompositesProvided,
`The composite attribute ${attrName} has been provided more than once with different values. Remove the duplication before running again`,
);
}
state.applyCondition(
FilterOperationNames.eq,
attrName,
composites[attrName],
);
}
}
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
append: {
name: "append",
action(entity, state, data = {}) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.append, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
ifNotExists: {
name: "ifNotExists",
action(entity, state, data = {}) {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.ifNotExists, data);
return state;
},
children: methodChildren.update,
},
upsertIfNotExists: {
name: "ifNotExists",
action(entity, state, data = {}) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data, { allowReadOnly: true });
state.query.upsert.addData(UpsertOperations.ifNotExists, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
upsertAppend: {
name: "append",
action(entity, state, data = {}) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data, { allowReadOnly: true });
state.query.upsert.addData(UpsertOperations.append, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
updateRemove: {
name: "remove",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
if (!Array.isArray(data)) {
throw new Error("Update method 'remove' expects type Array");
}
entity.model.schema.checkRemove(data);
state.query.updateProxy.fromArray(ItemOperations.remove, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
updateDelete: {
name: "delete",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.delete, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
add: {
name: "add",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.add, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
upsertAdd: {
name: "add",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data, { allowReadOnly: true });
state.query.upsert.addData(UpsertOperations.add, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
upsertSubtract: {
name: "subtract",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data, { allowReadOnly: true });
state.query.upsert.addData(UpsertOperations.subtract, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.upsert,
},
subtract: {
name: "subtract",
action(entity, state, data) {
if (state.getError() !== null) {
return state;
}
try {
entity.model.schema.checkUpdate(data);
state.query.updateProxy.fromObject(ItemOperations.subtract, data);
return state;
} catch (err) {
state.setError(err);
return state;
}
},
children: methodChildren.update,
},
query: {
name: "query",
action(entity, state, facets, options = {}) {
if (state.getError() !== null) {
return state;
}
try {
state.addOption("_isPagination", true);
const { pk, sk } = state.getCompositeAttributes();
return state
.setMethod(MethodTypes.query)
.setType(QueryTypes.is)
.setPK(entity._expectFacets(facets, pk))
.ifSK(() => {
const { composites, unused } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
state.setSK(state.buildQueryComposites(facets, sk));
state.whenOptions(({ options, state }) => {
if (
options.compare === ComparisonTypes.attributes ||
options.compare === ComparisonTypes.v2
) {
if (sk.length > 1) {
state.filterProperties(FilterOperationNames.eq, {
...unused,
...composites,
});
}
}
if (
state.query.options.indexType === IndexTypes.clustered &&
Object.keys(composites).length < sk.length &&
!options.ignoreOwnership &&
!state.getParams()
) {
state
.unsafeApplyFilter(
{},
FilterOperationNames.eq,
entity.identifiers.entity,
entity.getName(),
)
.unsafeApplyFilter(
{},
FilterOperationNames.eq,
entity.identifiers.version,
entity.getVersion(),
);
}
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["between", "gte", "gt", "lte", "lt", "begins", "params", "go"],
},
between: {
name: "between",
action(entity, state, startingFacets = {}, endingFacets = {}) {
if (state.getError() !== null) {
return state;
}
try {
const { pk, sk } = state.getCompositeAttributes();
const endingSk = state.identifyCompositeAttributes(
endingFacets,
sk,
pk,
);
const startingSk = state.identifyCompositeAttributes(
startingFacets,
sk,
pk,
);
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
return state
.setType(QueryTypes.and)
.setSK(endingSk.composites)
.setType(QueryTypes.between)
.setSK(startingSk.composites)
.beforeBuildParams(({ options, state }) => {
if (
options.compare === ComparisonTypes.attributes ||
options.compare === ComparisonTypes.v2
) {
if (!entity.model.indexes[accessPattern].sk.isFieldRef) {
state.filterProperties(
FilterOperationNames.lte,
endingSk.composites,
{ asPrefix: true },
);
}
if (options.compare === ComparisonTypes.attributes) {
if (!entity.model.indexes[accessPattern].sk.isFieldRef) {
state.filterProperties(
FilterOperationNames.gte,
startingSk.composites,
{ asPrefix: true },
);
}
}
}
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
begins: {
name: "begins",
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state.setType(QueryTypes.begins).ifSK((state) => {
const attributes = state.getCompositeAttributes();
state.setSK(state.buildQueryComposites(facets, attributes.sk));
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
gt: {
name: "gt",
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state.setType(QueryTypes.gt).ifSK((state) => {
const { pk, sk } = state.getCompositeAttributes();
const { composites } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
state.setSK(composites);
state.beforeBuildParams(({ options, state }) => {
if (
options.compare === ComparisonTypes.attributes ||
options.compare === ComparisonTypes.v2
) {
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
if (!entity.model.indexes[accessPattern].sk.isFieldRef) {
state.filterProperties(FilterOperationNames.gt, composites, {
asPrefix: true,
});
}
}
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
gte: {
name: "gte",
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state.setType(QueryTypes.gte).ifSK((state) => {
const attributes = state.getCompositeAttributes();
state.setSK(state.buildQueryComposites(facets, attributes.sk));
state.beforeBuildParams(({ options, state }) => {
const { composites } = state.identifyCompositeAttributes(
facets,
attributes.sk,
attributes.pk,
);
if (options.compare === ComparisonTypes.attributes) {
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
if (
!entity.model.indexes[accessPattern].sk.isFieldRef &&
attributes.sk.length > 1
) {
state.filterProperties(FilterOperationNames.gte, composites, {
asPrefix: true,
});
}
}
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
lt: {
name: "lt",
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state.setType(QueryTypes.lt).ifSK((state) => {
const { pk, sk } = state.getCompositeAttributes();
const { composites } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
state.setSK(composites);
state.beforeBuildParams(({ options, state }) => {
if (options.compare === ComparisonTypes.attributes) {
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
if (!entity.model.indexes[accessPattern].sk.isFieldRef) {
state.filterProperties(FilterOperationNames.lt, composites, {
asPrefix: true,
});
}
}
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
lte: {
name: "lte",
action(entity, state, facets = {}) {
if (state.getError() !== null) {
return state;
}
try {
return state.setType(QueryTypes.lte).ifSK((state) => {
const { pk, sk } = state.getCompositeAttributes();
const { composites } = state.identifyCompositeAttributes(
facets,
sk,
pk,
);
state.setSK(composites);
state.beforeBuildParams(({ options, state }) => {
if (
options.compare === ComparisonTypes.attributes ||
options.compare === ComparisonTypes.v2
) {
const accessPattern =
entity.model.translations.indexes.fromIndexToAccessPattern[
state.query.index
];
if (!entity.model.indexes[accessPattern].sk.isFieldRef) {
state.filterProperties(FilterOperationNames.lte, composites, {
asPrefix: true,
});
}
}
});
});
} catch (err) {
state.setError(err);
return state;
}
},
children: ["go", "params"],
},
commit: {
name: "commit",
action(entity, state, options) {
if (state.getError() !== null) {
throw state.error;
}
const results = clauses.params.action(entity, state, {
...options,
_returnOptions: true,
_isTransaction: true,
});
const method = TransactionOperations[state.query.method];
if (!method) {
throw new Error("Invalid commit method");
}
return {
[method]: results.params,
[TransactionCommitSymbol]: () => {
return {
entity,
};
},
};
},
children: [],
},
params: {
name: "params",
action(entity, state, options = {}) {
if (state.getError() !== null) {
throw state.error;
}
try {
if (
!v.isStringHasLength(options.table) &&
!v.isStringHasLength(entity.getTableName())
) {
throw new e.ElectroError(
e.ErrorCodes.MissingTable,
`Table name not defined. Table names must be either defined on the model, instance configuration, or as a query option.`,
);
}
const method = state.getMethod();
const normalizedOptions = entity._normalizeExecutionOptions({
provided: [state.getOptions(), state.query.options, options],
context: {
operation: options._isTransaction
? MethodTypes.transactWrite
: undefined,
},
});
state.applyWithOptions(normalizedOptions);
state.applyBeforeBuildParams(normalizedOptions);
let results;
switch (method) {
case MethodTypes.query: {
results = entity._queryParams(state, normalizedOptions);
break;
}
case MethodTypes.batchWrite: {
results = entity._batchWriteParams(state, normalizedOptions);
break;
}
case MethodTypes.batchGet: {
results = entity._batchGetParams(state, normalizedOptions);
break;
}
default: {
results = entity._params(state, normalizedOptions);
break;
}
}
if (
method === MethodTypes.update &&
results.ExpressionAttributeValues &&
Object.keys(results.ExpressionAttributeValues).length === 0
) {
// An update that only does a `remove` operation would result in an empty object
// todo: change the getValues() method to return undefined in this case (would potentially require a more generous refactor)
delete results.ExpressionAttributeValues;
}
if (options._returnOptions) {
results = {
params: results,
options: normalizedOptions,
};
}
state.setParams(results);
return results;
} catch (err) {
throw err;
}
},
children: [],
},
go: {
name: "go",
action(entity, state, options = {}) {
if (state.getError() !== null) {
return Promise.reject(state.error);
}
try {
if (entity.client === undefined) {
throw new e.ElectroError(
e.ErrorCodes.NoClientDefined,
"No client defined on model",
);
}
options.terminalOperation = TerminalOperation.go;
const paramResults = clauses.params.action(entity, state, {
...options,
_returnOptions: true,
});
return entity.go(
state.getMethod(),
paramResults.params,
paramResults.options,
);
} catch (err) {
return Promise.reject(err);
}
},
children: [],
},
};
class ChainState {
constructor({
index = "",
compositeAttributes = {},
attributes = {},
hasSortKey = false,
options = {},
parentState = null,
} = {}) {
const update = new UpdateExpression({ prefix: "_u" });
this.parentState = parentState;
this.error = null;
this.attributes = attributes;
this.query = {
collection: "",
index: index,
type: "",
method: "",
facets: { ...compositeAttributes },
update,
updateProxy: new AttributeOperationProxy({
builder: update,
attributes: attributes,
operations: UpdateOperations,
}),
put: {
data: {},
},
upsert: {
data: {},
indexKey: null,
addData(operation = UpsertOperations.set, data = {}) {
for (const name of Object.keys(data)) {
const value = data[name];
this.data[name] = {
operation,
value,
};
}
},
getData(operationFilter) {
const results = {};
for (const name in this.data) {
const { operation, value } = this.data[name];
if (!operationFilter || operationFilter === operation) {
results[name] = value;
}
}
return results;
},
},
keys: {
provided: [],
pk: {},
sk: [],
},
filter: {
[ExpressionTypes.ConditionExpression]: new FilterExpression(),
[ExpressionTypes.FilterExpression]: new FilterExpression(),
},
options,
};
this.subStates = [];
this.hasSortKey = hasSortKey;
this.prev = null;
this.self = null;
this.params = null;
this.applyAfterOptions = [];
this.beforeBuildParamsOperations = [];
this.beforeBuildParamsHasRan = false;
}
getParams() {
return this.params;
}
setParams(params) {
if (params) {
this.params = params;
}
}
init(entity, allClauses, currentClause) {
let current = {};
for (let child of currentClause.children) {
const name = allClauses[child].name;
current[name] = (...args) => {
this.prev = this.self;
this.self = child;
let results = allClauses[child].action(entity, this, ...args);
if (allClauses[child].children.length) {
return this.init(entity, allClauses, allClauses[child]);
} else {
return results;
}
};
}
return current;
}
getMethod() {
return this.query.method;
}
getOptions() {
return this.query.options;
}
addOption(key, value) {
this.query.options[key] = value;
return this;
}
_appendProvided(type, attributes) {
const newAttributes = Object.keys(attributes).map((attribute) => {
return {
type,
attribute,
};
});
return u.getUnique(this.query.keys.provided, newAttributes);
}
setPK(attributes) {
this.query.keys.pk = attributes;
this.query.keys.provided = this._appendProvided(KeyTypes.pk, attributes);
return this;
}
ifSK(cb) {
if (this.hasSortKey) {
cb(this);
}
return this;
}
getCompositeAttributes() {
return this.query.facets;
}
buildQueryComposites(provided, definition) {
return definition
.map((name) => [name, provided[name]])
.reduce((result, [name, value]) => {
if (value !== undefined) {
result[name] = value;
}
return result;
}, {});
}
identifyCompositeAttributes(provided, defined, skip) {
// todo: make sure attributes are valid
const composites = {};
const unused = {};
const definedSet = new Set(defined || []);
const skipSet = new Set(skip || []);
for (const key of Object.keys(provided)) {
const value = provided[key];
if (definedSet.has(key)) {
composites[key] = value;
} else if (skipSet.has(key)) {
continue;
} else {
unused[key] = value;
}
}
return {
composites,
unused,
};
}
applyFilter(operation, name, values, filterOptions) {
if (
FilterOperationNames[operation] !== undefined &&
name !== undefined &&
values !== undefined
) {
const attribute = this.attributes[name];
if (attribute !== undefined) {
this.unsafeApplyFilter(
filterOptions,
operation,
attribute.field,
values,
);
}
}
return this;
}
applyCondition(operation, name, ...values) {
if (
FilterOperationNames[operation] !== undefined &&
name !== undefined &&
values.length > 0
) {
const attribute = this.attributes[name];
if (attribute !== undefined) {
const filter = this.query.filter[ExpressionTypes.ConditionExpression];
filter.unsafeSet({}, operation, attribute.field, ...values);
}
}
return this;
}
unsafeApplyFilter(filterOptions = {}, operation, name, values) {
if (
(FilterOperationNames[operation] !== undefined) & (name !== undefined) &&
values !== undefined
) {
const filter = this.query.filter[ExpressionTypes.FilterExpression];
filter.unsafeSet(filterOptions, operation, name, values);
}
return this;
}
filterProperties(operation, obj = {}, filterOptions = {}) {
for (const property in obj) {
const value = obj[property];
if (value !== undefined) {
this.applyFilter(operation, property, value, filterOptions);
}
}
return this;
}
setSK(attributes, type = this.query.type) {
if (this.hasSortKey) {
this.query.keys.sk.push({
type: type,
facets: attributes,
});
this.query.keys.provided = this._appendProvided(KeyTypes.sk, attributes);
}
return this;
}
setType(type) {
if (!QueryTypes[type]) {
throw new Error(`Invalid query type: "${type}"`);
}
this.query.type = QueryTypes[type];
return this;
}
setMethod(method) {
if (!MethodTypes[method]) {
throw new Error(`Invalid method type: "${method}"`);
}
this.query.method = MethodTypes[method];
return this;
}
setCollection(collection) {
this.query.collection = collection;
return this;
}
createSubState() {
let subState = new ChainState({
parentState: this,
index: this.query.index,
attributes: this.attributes,
hasSortKey: this.hasSortKey,
options: this.query.options,
compositeAttributes: this.query.facets,
});
this.subStates.push(subState);
return subState;
}
getError() {
return this.error;
}
setError(err) {
this.error = err;
if (this.parentState) {
this.parentState.setError(err);
}
}
applyUpsert(operation = UpsertOperations.set, data = {}) {
this.query.upsert.addData(operation, data);
return this;
}
applyPut(data = {}) {
this.query.put.data = { ...this.query.put.data, ...data };
return this;
}
whenOptions(fn) {
if (v.isFunction(fn)) {
this.applyAfterOptions.push((options) => {
fn({ options, state: this });
});
}
return this;
}
// these are ran before "beforeBuildParams"
applyWithOptions(options = {}) {
this.applyAfterOptions.forEach((fn) => fn(options));
}
beforeBuildParams(fn) {
if (v.isFunction(fn)) {
this.beforeBuildParamsOperations.push((options) => {
fn({ options, state: this });
});
}
return this;
}
applyBeforeBuildParams(options = {}) {
if (!this.beforeBuildParamsHasRan) {
this.beforeBuildParamsHasRan = true;
this.beforeBuildParamsOperations.forEach((fn) => fn(options));
}
}
}
module.exports = {
clauses,
ChainState,
};