electrodb 3.5.3 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +627 -276
- package/index.js +5 -1
- package/package.json +1 -1
- package/src/clauses.js +1 -0
- package/src/client.js +6 -0
- package/src/entity.js +171 -89
- package/src/errors.js +19 -12
- package/src/operations.js +33 -23
- package/src/schema.js +25 -2
- package/src/service.js +23 -12
- package/src/types.js +15 -0
- package/src/util.js +11 -0
- package/src/validations.js +22 -1
package/index.js
CHANGED
|
@@ -18,7 +18,9 @@ const {
|
|
|
18
18
|
const { createConversions } = require("./src/conversions");
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
|
-
ComparisonTypes
|
|
21
|
+
ComparisonTypes,
|
|
22
|
+
EntityIdentifiers,
|
|
23
|
+
EntityIdentifierFields,
|
|
22
24
|
} = require('./src/types');
|
|
23
25
|
|
|
24
26
|
module.exports = {
|
|
@@ -35,4 +37,6 @@ module.exports = {
|
|
|
35
37
|
ElectroUserValidationError,
|
|
36
38
|
ElectroAttributeValidationError,
|
|
37
39
|
createConversions,
|
|
40
|
+
EntityIdentifiers,
|
|
41
|
+
EntityIdentifierFields,
|
|
38
42
|
};
|
package/package.json
CHANGED
package/src/clauses.js
CHANGED
package/src/client.js
CHANGED
|
@@ -2,6 +2,7 @@ const lib = require("@aws-sdk/lib-dynamodb");
|
|
|
2
2
|
const util = require("@aws-sdk/util-dynamodb");
|
|
3
3
|
const { isFunction } = require("./validations");
|
|
4
4
|
const { ElectroError, ErrorCodes } = require("./errors");
|
|
5
|
+
const { EntityIdentifiers } = require("./types");
|
|
5
6
|
const DocumentClientVersions = {
|
|
6
7
|
v2: "v2",
|
|
7
8
|
v3: "v3",
|
|
@@ -289,9 +290,14 @@ function normalizeClient(client) {
|
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
function normalizeConfig(config = {}) {
|
|
293
|
+
const identifiers = config.identifiers || {};
|
|
292
294
|
return {
|
|
293
295
|
...config,
|
|
294
296
|
client: normalizeClient(config.client),
|
|
297
|
+
identifiers: {
|
|
298
|
+
entity: identifiers.entity || EntityIdentifiers.entity,
|
|
299
|
+
version: identifiers.version || EntityIdentifiers.version
|
|
300
|
+
}
|
|
295
301
|
};
|
|
296
302
|
}
|
|
297
303
|
|
package/src/entity.js
CHANGED
|
@@ -30,9 +30,10 @@ const {
|
|
|
30
30
|
CastKeyOptions,
|
|
31
31
|
ComparisonTypes,
|
|
32
32
|
DataOptions,
|
|
33
|
+
IndexProjectionOptions,
|
|
33
34
|
} = require("./types");
|
|
34
35
|
const { FilterFactory } = require("./filters");
|
|
35
|
-
const { FilterOperations } = require("./operations");
|
|
36
|
+
const { FilterOperations, formatExpressionName } = require("./operations");
|
|
36
37
|
const { WhereFactory } = require("./where");
|
|
37
38
|
const { clauses, ChainState } = require("./clauses");
|
|
38
39
|
const { EventManager } = require("./events");
|
|
@@ -49,16 +50,16 @@ const ImpactedIndexTypeSource = {
|
|
|
49
50
|
|
|
50
51
|
class Entity {
|
|
51
52
|
constructor(model, config = {}) {
|
|
52
|
-
config = c.normalizeConfig(config);
|
|
53
|
+
this.config = c.normalizeConfig(config);
|
|
54
|
+
this.identifiers = this.config.identifiers;
|
|
55
|
+
this.client = this.config.client;
|
|
53
56
|
this.eventManager = new EventManager({
|
|
54
|
-
listeners: config.listeners,
|
|
57
|
+
listeners: this.config.listeners,
|
|
55
58
|
});
|
|
56
|
-
this.eventManager.add(config.logger);
|
|
59
|
+
this.eventManager.add(this.config.logger);
|
|
57
60
|
this._validateModel(model);
|
|
58
61
|
this.version = EntityVersions.v1;
|
|
59
|
-
this.
|
|
60
|
-
this.client = config.client;
|
|
61
|
-
this.model = this._parseModel(model, config);
|
|
62
|
+
this.model = this._parseModel(model, this.config);
|
|
62
63
|
/** start beta/v1 condition **/
|
|
63
64
|
this.config.table = config.table || model.table;
|
|
64
65
|
/** end beta/v1 condition **/
|
|
@@ -94,24 +95,31 @@ class Entity {
|
|
|
94
95
|
).query(...values);
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
this.config.identifiers = config.identifiers || {};
|
|
99
|
-
this.identifiers = {
|
|
100
|
-
entity: this.config.identifiers.entity || "__edb_e__",
|
|
101
|
-
version: this.config.identifiers.version || "__edb_v__",
|
|
102
|
-
};
|
|
103
98
|
this._instance = ElectroInstance.entity;
|
|
104
99
|
this._instanceType = ElectroInstanceTypes.entity;
|
|
105
100
|
this.schema = model;
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
get scan() {
|
|
109
|
-
|
|
104
|
+
const result = this._makeChain(
|
|
110
105
|
TableIndex,
|
|
111
106
|
this._clausesWithFilters,
|
|
112
107
|
clauses.index,
|
|
113
108
|
{ _isPagination: true },
|
|
114
109
|
).scan();
|
|
110
|
+
|
|
111
|
+
for (const accessPattern in this.model.indexes) {
|
|
112
|
+
const index = this.model.indexes[accessPattern].index;
|
|
113
|
+
|
|
114
|
+
result[accessPattern] = this._makeChain(
|
|
115
|
+
index,
|
|
116
|
+
this._clausesWithFilters,
|
|
117
|
+
clauses.index,
|
|
118
|
+
{ _isPagination: true },
|
|
119
|
+
).scan();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
setIdentifier(type = "", identifier = "") {
|
|
@@ -145,40 +153,38 @@ class Entity {
|
|
|
145
153
|
);
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
|
|
156
|
+
_itemIncludesKeys(item) {
|
|
149
157
|
let { pk, sk } = this.model.prefixes[TableIndex];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const attribute = attributes[i];
|
|
154
|
-
if (attribute === sk.field) {
|
|
155
|
-
skFound = true;
|
|
156
|
-
}
|
|
157
|
-
if (attribute === pk.field) {
|
|
158
|
-
skFound = true;
|
|
159
|
-
}
|
|
160
|
-
if (pkFound && skFound) {
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return false;
|
|
158
|
+
return item[pk.field] !== undefined && (
|
|
159
|
+
sk.field === undefined || item[sk.field] !== undefined
|
|
160
|
+
);
|
|
165
161
|
}
|
|
166
162
|
|
|
167
163
|
ownsKeys(key = {}) {
|
|
164
|
+
const accessPattern =
|
|
165
|
+
this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
|
|
168
166
|
let { pk, sk } = this.model.prefixes[TableIndex];
|
|
169
167
|
let hasSK = this.model.lookup.indexHasSortKeys[TableIndex];
|
|
170
168
|
const typeofPkProvided = typeof key[pk.field];
|
|
171
169
|
const pkPrefixMatch =
|
|
172
|
-
typeofPkProvided === "string" &&
|
|
170
|
+
typeofPkProvided === "string" &&
|
|
171
|
+
key[pk.field].startsWith(pk.prefix) &&
|
|
172
|
+
(!pk.postfix || key[pk.field].endsWith(pk.postfix));
|
|
173
173
|
const isNumericPk = typeofPkProvided === "number" && pk.cast === "number";
|
|
174
174
|
let pkMatch = pkPrefixMatch || isNumericPk;
|
|
175
175
|
let skMatch = pkMatch && !hasSK;
|
|
176
176
|
if (pkMatch && hasSK) {
|
|
177
177
|
const typeofSkProvided = typeof key[sk.field];
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
if (typeofSkProvided === "number") {
|
|
179
|
+
skMatch = sk.cast === "number";
|
|
180
|
+
} else if (typeofSkProvided === "string") {
|
|
181
|
+
// if sk is a string, check prefix and postfix match
|
|
182
|
+
const hasNoPrefixOrStartsWithPrefix = !sk.prefix || key[sk.field].startsWith(sk.prefix);
|
|
183
|
+
const hasNoPostfixOrEndsWithPostfix = !sk.postfix || key[sk.field].endsWith(sk.postfix);
|
|
184
|
+
skMatch =
|
|
185
|
+
hasNoPrefixOrStartsWithPrefix &&
|
|
186
|
+
hasNoPostfixOrEndsWithPostfix;
|
|
187
|
+
}
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
return (
|
|
@@ -652,6 +658,7 @@ class Entity {
|
|
|
652
658
|
_isCollectionQuery: false,
|
|
653
659
|
preserveBatchOrder: true,
|
|
654
660
|
ignoreOwnership: config._providedIgnoreOwnership,
|
|
661
|
+
attributes: config._providedAttributes,
|
|
655
662
|
});
|
|
656
663
|
|
|
657
664
|
const unprocessed = [];
|
|
@@ -689,7 +696,9 @@ class Entity {
|
|
|
689
696
|
let iterations = 0;
|
|
690
697
|
let count = 0;
|
|
691
698
|
let hydratedUnprocessed = [];
|
|
692
|
-
const shouldHydrate =
|
|
699
|
+
const shouldHydrate =
|
|
700
|
+
config.hydrate &&
|
|
701
|
+
(method === MethodTypes.query || method === MethodTypes.scan);
|
|
693
702
|
do {
|
|
694
703
|
let response = await this._exec(
|
|
695
704
|
method,
|
|
@@ -908,6 +917,18 @@ class Entity {
|
|
|
908
917
|
}
|
|
909
918
|
}
|
|
910
919
|
|
|
920
|
+
is(item, config) {
|
|
921
|
+
return (
|
|
922
|
+
config.ignoreOwnership &&
|
|
923
|
+
!this._itemIncludesKeys(item)
|
|
924
|
+
) || (
|
|
925
|
+
(config.ignoreOwnership || config.hydrate) &&
|
|
926
|
+
this.ownsKeys(item)
|
|
927
|
+
) || (
|
|
928
|
+
this.ownsItem(item)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
911
932
|
formatResponse(response, index, config = {}) {
|
|
912
933
|
let stackTrace;
|
|
913
934
|
if (!config.originalErr) {
|
|
@@ -930,15 +951,7 @@ class Entity {
|
|
|
930
951
|
results = response;
|
|
931
952
|
} else {
|
|
932
953
|
if (response.Item) {
|
|
933
|
-
if (
|
|
934
|
-
(config.ignoreOwnership &&
|
|
935
|
-
config.attributes &&
|
|
936
|
-
config.attributes.length > 0 &&
|
|
937
|
-
!this._attributesIncludeKeys(config.attributes)) ||
|
|
938
|
-
((config.ignoreOwnership || config.hydrate) &&
|
|
939
|
-
this.ownsKeys(response.Item)) ||
|
|
940
|
-
this.ownsItem(response.Item)
|
|
941
|
-
) {
|
|
954
|
+
if (this.is(response.Item, config)) {
|
|
942
955
|
results = this.model.schema.formatItemForRetrieval(
|
|
943
956
|
response.Item,
|
|
944
957
|
config,
|
|
@@ -949,15 +962,7 @@ class Entity {
|
|
|
949
962
|
} else if (response.Items) {
|
|
950
963
|
results = [];
|
|
951
964
|
for (let item of response.Items) {
|
|
952
|
-
if (
|
|
953
|
-
(config.ignoreOwnership &&
|
|
954
|
-
config.attributes &&
|
|
955
|
-
config.attributes.length > 0 &&
|
|
956
|
-
!this._attributesIncludeKeys(config.attributes)) ||
|
|
957
|
-
((config.ignoreOwnership || config.hydrate) &&
|
|
958
|
-
this.ownsKeys(item)) ||
|
|
959
|
-
this.ownsItem(item)
|
|
960
|
-
) {
|
|
965
|
+
if (this.is(item, config)) {
|
|
961
966
|
let record = this.model.schema.formatItemForRetrieval(
|
|
962
967
|
item,
|
|
963
968
|
config,
|
|
@@ -1640,6 +1645,7 @@ class Entity {
|
|
|
1640
1645
|
listeners: [],
|
|
1641
1646
|
preserveBatchOrder: false,
|
|
1642
1647
|
attributes: [],
|
|
1648
|
+
_providedAttributes: [],
|
|
1643
1649
|
terminalOperation: undefined,
|
|
1644
1650
|
formatCursor: u.cursorFormatter,
|
|
1645
1651
|
order: undefined,
|
|
@@ -1649,6 +1655,19 @@ class Entity {
|
|
|
1649
1655
|
_includeOnResponseItem: {},
|
|
1650
1656
|
};
|
|
1651
1657
|
|
|
1658
|
+
// Auto-set ignoreOwnership: true for INCLUDE or KEYS_ONLY indexes
|
|
1659
|
+
if (context.state && context.state.query && context.state.query.index) {
|
|
1660
|
+
const indexName = context.state.query.index;
|
|
1661
|
+
const accessPattern =
|
|
1662
|
+
this.model.translations.indexes.fromIndexToAccessPattern[indexName];
|
|
1663
|
+
if (accessPattern) {
|
|
1664
|
+
const indexDefinition = this.model.indexes[accessPattern];
|
|
1665
|
+
config.ignoreOwnership =
|
|
1666
|
+
indexDefinition.projection === IndexProjectionOptions.keys_only ||
|
|
1667
|
+
(Array.isArray(indexDefinition.projection) && !this.model.indexes[accessPattern].identifiersAreProjected);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1652
1671
|
return provided.filter(Boolean).reduce((config, option) => {
|
|
1653
1672
|
if (typeof option.order === "string") {
|
|
1654
1673
|
switch (option.order.toLowerCase()) {
|
|
@@ -1727,6 +1746,7 @@ class Entity {
|
|
|
1727
1746
|
|
|
1728
1747
|
if (Array.isArray(option.attributes)) {
|
|
1729
1748
|
config.attributes = config.attributes.concat(option.attributes);
|
|
1749
|
+
config._providedAttributes = option.attributes;
|
|
1730
1750
|
}
|
|
1731
1751
|
|
|
1732
1752
|
if (option.preserveBatchOrder === true) {
|
|
@@ -1850,7 +1870,7 @@ class Entity {
|
|
|
1850
1870
|
}
|
|
1851
1871
|
}
|
|
1852
1872
|
|
|
1853
|
-
if (option.ignoreOwnership) {
|
|
1873
|
+
if (option.ignoreOwnership !== undefined) {
|
|
1854
1874
|
config.ignoreOwnership = option.ignoreOwnership;
|
|
1855
1875
|
config._providedIgnoreOwnership = option.ignoreOwnership;
|
|
1856
1876
|
}
|
|
@@ -1875,6 +1895,10 @@ class Entity {
|
|
|
1875
1895
|
if (option.hydrate) {
|
|
1876
1896
|
config.hydrate = true;
|
|
1877
1897
|
config.ignoreOwnership = true;
|
|
1898
|
+
// if we will hydrate later, we don't want to provide a ProjectionExpression since the attributes
|
|
1899
|
+
// may contain non-projected attributes that the user expects to receive from the main table
|
|
1900
|
+
// after hydration
|
|
1901
|
+
config.attributes = [];
|
|
1878
1902
|
}
|
|
1879
1903
|
|
|
1880
1904
|
if (validations.isFunction(option.hydrator)) {
|
|
@@ -2015,6 +2039,7 @@ class Entity {
|
|
|
2015
2039
|
params = this._makeScanParam(
|
|
2016
2040
|
filter[ExpressionTypes.FilterExpression],
|
|
2017
2041
|
config,
|
|
2042
|
+
state,
|
|
2018
2043
|
);
|
|
2019
2044
|
break;
|
|
2020
2045
|
/* istanbul ignore next */
|
|
@@ -2058,27 +2083,41 @@ class Entity {
|
|
|
2058
2083
|
TerminalOperation[config.terminalOperation] === TerminalOperation.go ||
|
|
2059
2084
|
TerminalOperation[config.terminalOperation] === TerminalOperation.page;
|
|
2060
2085
|
|
|
2061
|
-
//
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
let
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2086
|
+
// Convert all attribute names to their respective "field" names
|
|
2087
|
+
let hasTableIndexPk = false;
|
|
2088
|
+
let hasTableIndexSk = !this.model.translations.keys[TableIndex].sk;
|
|
2089
|
+
const attributeFields = new Map();
|
|
2090
|
+
for (let [key, name] of Object.entries(parameters.ExpressionAttributeNames || {})) {
|
|
2091
|
+
if (key.startsWith("#")) {
|
|
2092
|
+
key = key.slice(1);
|
|
2093
|
+
}
|
|
2094
|
+
attributeFields.set(key, name);
|
|
2095
|
+
if (name === this.model.translations.keys[TableIndex].pk) {
|
|
2096
|
+
hasTableIndexPk = true;
|
|
2072
2097
|
}
|
|
2098
|
+
if (name === this.model.translations.keys[TableIndex].sk) {
|
|
2099
|
+
hasTableIndexSk = true;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (!hasTableIndexPk) {
|
|
2104
|
+
const field = this.model.translations.keys[TableIndex].pk;
|
|
2105
|
+
const name = formatExpressionName(field, attributeFields);
|
|
2106
|
+
attributeFields.set(name, field);
|
|
2073
2107
|
}
|
|
2074
2108
|
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2109
|
+
if (!hasTableIndexSk) {
|
|
2110
|
+
const field = this.model.translations.keys[TableIndex].sk;
|
|
2111
|
+
const name = formatExpressionName(field, attributeFields);
|
|
2112
|
+
attributeFields.set(name, field);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
for (const attributeName of attributes) {
|
|
2116
|
+
const fieldName = this.model.schema.getFieldName(attributeName) || attributeName;
|
|
2117
|
+
if (fieldName) {
|
|
2118
|
+
const formatted = formatExpressionName(fieldName, attributeFields);
|
|
2119
|
+
attributeFields.set(formatted, fieldName);
|
|
2080
2120
|
}
|
|
2081
|
-
throw new e.ElectroError(e.ErrorCodes.InvalidOptions, message);
|
|
2082
2121
|
}
|
|
2083
2122
|
|
|
2084
2123
|
// add ExpressionAttributeNames if it doesn't exist already
|
|
@@ -2095,8 +2134,8 @@ class Entity {
|
|
|
2095
2134
|
enforcesOwnership
|
|
2096
2135
|
) {
|
|
2097
2136
|
// add entity identifiers to so items can be identified
|
|
2098
|
-
attributeFields.
|
|
2099
|
-
attributeFields.
|
|
2137
|
+
attributeFields.set(this.identifiers.entity, this.identifiers.entity);
|
|
2138
|
+
attributeFields.set(this.identifiers.version, this.identifiers.version);
|
|
2100
2139
|
|
|
2101
2140
|
// if pagination is required you may enter into a scenario where
|
|
2102
2141
|
// the LastEvaluatedKey doesn't belong to entity and one must be formed.
|
|
@@ -2111,22 +2150,23 @@ class Entity {
|
|
|
2111
2150
|
|
|
2112
2151
|
for (const attribute of [...tableIndexFacets.all, ...indexFacets.all]) {
|
|
2113
2152
|
const fieldName = this.model.schema.getFieldName(attribute.name);
|
|
2114
|
-
attributeFields
|
|
2153
|
+
const formatted = formatExpressionName(attribute.name, attributeFields);
|
|
2154
|
+
attributeFields.set(formatted, fieldName);
|
|
2115
2155
|
}
|
|
2116
2156
|
}
|
|
2117
2157
|
}
|
|
2118
2158
|
|
|
2119
|
-
for (const attributeField of attributeFields) {
|
|
2159
|
+
for (const [attributeField, attributeName] of attributeFields.entries()) {
|
|
2120
2160
|
// prefix the ExpressionAttributeNames because some prefixes are not allowed
|
|
2121
2161
|
parameters.ExpressionAttributeNames["#" + attributeField] =
|
|
2122
|
-
|
|
2162
|
+
attributeName;
|
|
2123
2163
|
}
|
|
2124
2164
|
|
|
2125
2165
|
// if there is already a ProjectionExpression (e.g. config "params"), merge it
|
|
2126
2166
|
if (typeof parameters.ProjectionExpression === "string") {
|
|
2127
2167
|
parameters.ProjectionExpression = [
|
|
2128
2168
|
parameters.ProjectionExpression,
|
|
2129
|
-
...Object.keys(
|
|
2169
|
+
...Object.keys(parameters.ExpressionAttributeNames),
|
|
2130
2170
|
].join(", ");
|
|
2131
2171
|
} else {
|
|
2132
2172
|
parameters.ProjectionExpression = Object.keys(
|
|
@@ -2234,17 +2274,17 @@ class Entity {
|
|
|
2234
2274
|
}
|
|
2235
2275
|
|
|
2236
2276
|
/* istanbul ignore next */
|
|
2237
|
-
_makeScanParam(filter = {}, options = {}) {
|
|
2238
|
-
let
|
|
2239
|
-
let hasSortKey = this.model.lookup.indexHasSortKeys[
|
|
2277
|
+
_makeScanParam(filter = {}, options = {}, state = {}) {
|
|
2278
|
+
let index = state.query.index || TableIndex;
|
|
2279
|
+
let hasSortKey = this.model.lookup.indexHasSortKeys[index];
|
|
2240
2280
|
let accessPattern =
|
|
2241
|
-
this.model.translations.indexes.fromIndexToAccessPattern[
|
|
2281
|
+
this.model.translations.indexes.fromIndexToAccessPattern[index];
|
|
2242
2282
|
let pkField = this.model.indexes[accessPattern].pk.field;
|
|
2243
2283
|
let { pk, sk } = this._makeIndexKeys({
|
|
2244
|
-
index
|
|
2284
|
+
index,
|
|
2245
2285
|
});
|
|
2246
2286
|
|
|
2247
|
-
let keys = this._makeParameterKey(
|
|
2287
|
+
let keys = this._makeParameterKey(index, pk, ...sk);
|
|
2248
2288
|
// trim empty key values (this can occur when keys are defined by users)
|
|
2249
2289
|
for (let key in keys) {
|
|
2250
2290
|
if (keys[key] === undefined || keys[key] === "") {
|
|
@@ -2297,6 +2337,10 @@ class Entity {
|
|
|
2297
2337
|
params.FilterExpression = filterExpressions.join(" AND ");
|
|
2298
2338
|
}
|
|
2299
2339
|
|
|
2340
|
+
if (index) {
|
|
2341
|
+
params.IndexName = index;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2300
2344
|
return this._applyProjectionExpressions({
|
|
2301
2345
|
parameters: params,
|
|
2302
2346
|
config: options,
|
|
@@ -4227,7 +4271,7 @@ class Entity {
|
|
|
4227
4271
|
return model;
|
|
4228
4272
|
}
|
|
4229
4273
|
|
|
4230
|
-
_normalizeIndexes(indexes) {
|
|
4274
|
+
_normalizeIndexes(indexes, config) {
|
|
4231
4275
|
let normalized = {};
|
|
4232
4276
|
let indexFieldTranslation = {};
|
|
4233
4277
|
let indexHasSortKeys = {};
|
|
@@ -4253,6 +4297,7 @@ class Entity {
|
|
|
4253
4297
|
fields: [],
|
|
4254
4298
|
attributes: [],
|
|
4255
4299
|
labels: {},
|
|
4300
|
+
projections: [],
|
|
4256
4301
|
};
|
|
4257
4302
|
let seenIndexes = {};
|
|
4258
4303
|
let seenIndexFields = {};
|
|
@@ -4370,7 +4415,7 @@ class Entity {
|
|
|
4370
4415
|
e.ErrorCodes.DuplicateIndexCompositeAttributes,
|
|
4371
4416
|
`The Access Pattern '${accessPattern}' contains duplicate references the composite attribute(s): ${u.commaSeparatedString(
|
|
4372
4417
|
duplicates,
|
|
4373
|
-
)}. Composite attributes can only be used more than once in an index if your sort key is
|
|
4418
|
+
)}. Composite attributes can only be used more than once in an index if your sort key is limited to a single attribute. This is to prevent unexpected runtime errors related to the inability to generate keys.`,
|
|
4374
4419
|
);
|
|
4375
4420
|
}
|
|
4376
4421
|
}
|
|
@@ -4387,8 +4432,39 @@ class Entity {
|
|
|
4387
4432
|
scope: indexScope,
|
|
4388
4433
|
condition: indexCondition,
|
|
4389
4434
|
conditionDefined: conditionDefined,
|
|
4435
|
+
projection: index.projection,
|
|
4436
|
+
identifiersAreProjected: false,
|
|
4390
4437
|
};
|
|
4391
4438
|
|
|
4439
|
+
let projections = [];
|
|
4440
|
+
|
|
4441
|
+
if (index.projection !== undefined) {
|
|
4442
|
+
if (typeof index.projection === "string" && (
|
|
4443
|
+
index.projection.toLowerCase() === IndexProjectionOptions.keys_only ||
|
|
4444
|
+
index.projection.toLowerCase() === IndexProjectionOptions.all
|
|
4445
|
+
)) {
|
|
4446
|
+
definition.projection = index.projection.toLowerCase();
|
|
4447
|
+
} else if (Array.isArray(index.projection) && index.projection.length > 0 && index.projection.every((attr) => typeof attr === "string")) {
|
|
4448
|
+
definition.projection = index.projection;
|
|
4449
|
+
let nameIdentifierPresent = false;
|
|
4450
|
+
let versionIdentiferPresent = false;
|
|
4451
|
+
for (const name of definition.projection) {
|
|
4452
|
+
projections.push({ name, accessPattern });
|
|
4453
|
+
if (name === config.identifiers.entity) {
|
|
4454
|
+
nameIdentifierPresent = true;
|
|
4455
|
+
} else if (name === config.identifiers.version) {
|
|
4456
|
+
versionIdentiferPresent = true;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
definition.identifiersAreProjected = nameIdentifierPresent && versionIdentiferPresent;
|
|
4460
|
+
} else {
|
|
4461
|
+
throw new e.ElectroError(
|
|
4462
|
+
e.ErrorCodes.InvalidProjectionDefinition,
|
|
4463
|
+
`The Access Pattern '${accessPattern}' contains an invalid "projection" value: ${u.toDisplayString(index.projection)}. Valid projection values include ${u.commaSeparatedString(Object.values(IndexProjectionOptions))}, or an array of attribute names with a length greater than one.`
|
|
4464
|
+
)
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4392
4468
|
indexHasSubCollections[indexName] =
|
|
4393
4469
|
inCollection && Array.isArray(collection);
|
|
4394
4470
|
|
|
@@ -4444,6 +4520,7 @@ class Entity {
|
|
|
4444
4520
|
};
|
|
4445
4521
|
|
|
4446
4522
|
facets.attributes = [...facets.attributes, ...attributes];
|
|
4523
|
+
facets.projections = [...facets.projections, ...projections];
|
|
4447
4524
|
|
|
4448
4525
|
facets.fields.push(pk.field);
|
|
4449
4526
|
|
|
@@ -4844,10 +4921,14 @@ class Entity {
|
|
|
4844
4921
|
indexHasSortKeys,
|
|
4845
4922
|
indexAccessPattern,
|
|
4846
4923
|
indexHasSubCollections,
|
|
4847
|
-
} = this._normalizeIndexes(model.indexes);
|
|
4924
|
+
} = this._normalizeIndexes(model.indexes, config);
|
|
4848
4925
|
let schema = new Schema(model.attributes, facets, {
|
|
4849
|
-
getClient: () => this.client,
|
|
4850
4926
|
isRoot: true,
|
|
4927
|
+
getClient: () => this.client,
|
|
4928
|
+
identifiers: {
|
|
4929
|
+
[config.identifiers.entity]: config.identifiers.entity,
|
|
4930
|
+
[config.identifiers.version]: config.identifiers.version,
|
|
4931
|
+
},
|
|
4851
4932
|
});
|
|
4852
4933
|
|
|
4853
4934
|
let filters = this._normalizeFilters(model.filters);
|
|
@@ -4951,6 +5032,7 @@ function matchToEntityAlias({
|
|
|
4951
5032
|
record,
|
|
4952
5033
|
entities = {},
|
|
4953
5034
|
allowMatchOnKeys = false,
|
|
5035
|
+
config = {},
|
|
4954
5036
|
} = {}) {
|
|
4955
5037
|
let entity;
|
|
4956
5038
|
if (paramItem && v.isFunction(paramItem[TransactionCommitSymbol])) {
|
|
@@ -4984,7 +5066,7 @@ function matchToEntityAlias({
|
|
|
4984
5066
|
// entityAlias = alias;
|
|
4985
5067
|
// }
|
|
4986
5068
|
// }
|
|
4987
|
-
} else if (entities[alias] && entities[alias].
|
|
5069
|
+
} else if (entities[alias] && entities[alias].is(record, config)) {
|
|
4988
5070
|
entityAlias = alias;
|
|
4989
5071
|
break;
|
|
4990
5072
|
}
|
package/src/errors.js
CHANGED
|
@@ -133,18 +133,6 @@ const ErrorCodes = {
|
|
|
133
133
|
name: "IncompatibleKeyCasing",
|
|
134
134
|
sym: ErrorCode,
|
|
135
135
|
},
|
|
136
|
-
InvalidListenerProvided: {
|
|
137
|
-
code: 1020,
|
|
138
|
-
section: "invalid-listener-provided",
|
|
139
|
-
name: "InvalidListenerProvided",
|
|
140
|
-
sym: ErrorCode,
|
|
141
|
-
},
|
|
142
|
-
InvalidLoggerProvided: {
|
|
143
|
-
code: 1020,
|
|
144
|
-
section: "invalid-listener-provided",
|
|
145
|
-
name: "InvalidListenerProvided",
|
|
146
|
-
sym: ErrorCode,
|
|
147
|
-
},
|
|
148
136
|
InvalidClientProvided: {
|
|
149
137
|
code: 1021,
|
|
150
138
|
section: "invalid-client-provided",
|
|
@@ -157,6 +145,24 @@ const ErrorCodes = {
|
|
|
157
145
|
name: "Inconsistent Index Definition",
|
|
158
146
|
sym: ErrorCode,
|
|
159
147
|
},
|
|
148
|
+
InvalidListenerProvided: {
|
|
149
|
+
code: 1023,
|
|
150
|
+
section: "invalid-listener-provided",
|
|
151
|
+
name: "InvalidListenerProvided",
|
|
152
|
+
sym: ErrorCode,
|
|
153
|
+
},
|
|
154
|
+
InvalidLoggerProvided: {
|
|
155
|
+
code: 1024,
|
|
156
|
+
section: "invalid-listener-provided",
|
|
157
|
+
name: "InvalidListenerProvided",
|
|
158
|
+
sym: ErrorCode,
|
|
159
|
+
},
|
|
160
|
+
InvalidProjectionDefinition: {
|
|
161
|
+
code: 1025,
|
|
162
|
+
section: "invalid-projection-definition",
|
|
163
|
+
name: "InvalidProjectionDefinition",
|
|
164
|
+
sym: ErrorCode,
|
|
165
|
+
},
|
|
160
166
|
MissingAttribute: {
|
|
161
167
|
code: 2001,
|
|
162
168
|
section: "missing-attribute",
|
|
@@ -292,6 +298,7 @@ class ElectroError extends Error {
|
|
|
292
298
|
if (code && code.sym === ErrorCode) {
|
|
293
299
|
detail = code;
|
|
294
300
|
}
|
|
301
|
+
this.cause = cause;
|
|
295
302
|
this._message = message;
|
|
296
303
|
// this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`;
|
|
297
304
|
this.message = makeMessage(message, detail.section);
|
package/src/operations.js
CHANGED
|
@@ -9,6 +9,37 @@ const { FilterOperations } = require("./filterOperations");
|
|
|
9
9
|
const e = require("./errors");
|
|
10
10
|
const u = require("./util");
|
|
11
11
|
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* formatExpressionName: formats a field name for expression attribute names parameters.
|
|
15
|
+
* @param {string} name
|
|
16
|
+
* @param {Map<string, string>} seen
|
|
17
|
+
*/
|
|
18
|
+
function formatExpressionName(name, seen) {
|
|
19
|
+
const nameWasNotANumber = isNaN(name);
|
|
20
|
+
const originalName = `${name}`;
|
|
21
|
+
let formattedName = originalName.replaceAll(/[^\w]/g, "");
|
|
22
|
+
|
|
23
|
+
if (formattedName.length === 0) {
|
|
24
|
+
formattedName = "p";
|
|
25
|
+
} else if (nameWasNotANumber !== isNaN(formattedName)) {
|
|
26
|
+
// name became number due to replace
|
|
27
|
+
formattedName = `p${formattedName}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const originalFormattedName = formattedName;
|
|
31
|
+
let nameSuffix = 1;
|
|
32
|
+
while (
|
|
33
|
+
seen.has(formattedName) &&
|
|
34
|
+
seen.get(formattedName) !== originalName
|
|
35
|
+
) {
|
|
36
|
+
formattedName = `${originalFormattedName}_${++nameSuffix}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
seen.set(formattedName, originalName);
|
|
40
|
+
return formattedName;
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
class ExpressionState {
|
|
13
44
|
constructor({ prefix } = {}) {
|
|
14
45
|
this.names = {};
|
|
@@ -30,29 +61,7 @@ class ExpressionState {
|
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
formatName(name = "") {
|
|
33
|
-
|
|
34
|
-
const originalName = `${name}`;
|
|
35
|
-
let formattedName = originalName.replaceAll(/[^\w]/g, "");
|
|
36
|
-
|
|
37
|
-
if (formattedName.length === 0) {
|
|
38
|
-
formattedName = "p";
|
|
39
|
-
} else if (nameWasNotANumber !== isNaN(formattedName)) {
|
|
40
|
-
// name became number due to replace
|
|
41
|
-
formattedName = `p${formattedName}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const originalFormattedName = formattedName;
|
|
45
|
-
let nameSuffix = 1;
|
|
46
|
-
|
|
47
|
-
while (
|
|
48
|
-
this.formattedNameToOriginalNameMap.has(formattedName) &&
|
|
49
|
-
this.formattedNameToOriginalNameMap.get(formattedName) !== originalName
|
|
50
|
-
) {
|
|
51
|
-
formattedName = `${originalFormattedName}_${++nameSuffix}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
this.formattedNameToOriginalNameMap.set(formattedName, originalName);
|
|
55
|
-
return formattedName;
|
|
64
|
+
return formatExpressionName(name, this.formattedNameToOriginalNameMap);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
// todo: make the structure: name, value, paths
|
|
@@ -403,4 +412,5 @@ module.exports = {
|
|
|
403
412
|
FilterOperationNames,
|
|
404
413
|
ExpressionState,
|
|
405
414
|
AttributeOperationProxy,
|
|
415
|
+
formatExpressionName,
|
|
406
416
|
};
|