electrodb 3.5.2 → 3.6.0
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 +173 -91
- 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/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
|
};
|
package/src/schema.js
CHANGED
|
@@ -1215,7 +1215,7 @@ class Schema {
|
|
|
1215
1215
|
constructor(
|
|
1216
1216
|
properties = {},
|
|
1217
1217
|
facets = {},
|
|
1218
|
-
{ traverser = new AttributeTraverser(), getClient, parent, isRoot } = {},
|
|
1218
|
+
{ traverser = new AttributeTraverser(), getClient, parent, isRoot, identifiers } = {},
|
|
1219
1219
|
) {
|
|
1220
1220
|
this._validateProperties(properties, parent);
|
|
1221
1221
|
let schema = Schema.normalizeAttributes(properties, facets, {
|
|
@@ -1223,6 +1223,7 @@ class Schema {
|
|
|
1223
1223
|
getClient,
|
|
1224
1224
|
parent,
|
|
1225
1225
|
isRoot,
|
|
1226
|
+
identifiers,
|
|
1226
1227
|
});
|
|
1227
1228
|
this.getClient = getClient;
|
|
1228
1229
|
this.attributes = schema.attributes;
|
|
@@ -1242,7 +1243,7 @@ class Schema {
|
|
|
1242
1243
|
static normalizeAttributes(
|
|
1243
1244
|
attributes = {},
|
|
1244
1245
|
facets = {},
|
|
1245
|
-
{ traverser, getClient, parent, isRoot } = {},
|
|
1246
|
+
{ traverser, getClient, parent, isRoot, identifiers = {} } = {},
|
|
1246
1247
|
) {
|
|
1247
1248
|
const attributeHasParent = !!parent;
|
|
1248
1249
|
let invalidProperties = [];
|
|
@@ -1540,11 +1541,33 @@ class Schema {
|
|
|
1540
1541
|
);
|
|
1541
1542
|
}
|
|
1542
1543
|
|
|
1544
|
+
let missingProjectionAttributes = Array.isArray(facets.projections)
|
|
1545
|
+
? facets.projections.filter(({ name }) => !normalized[name] && !identifiers[name])
|
|
1546
|
+
: [];
|
|
1547
|
+
|
|
1548
|
+
if (missingProjectionAttributes.length > 0) {
|
|
1549
|
+
const byAccessPattern = facets.projections.reduce((groups, { name, accessPattern }) => {
|
|
1550
|
+
groups[accessPattern] = groups[accessPattern] || [];
|
|
1551
|
+
groups[accessPattern].push(name);
|
|
1552
|
+
return groups;
|
|
1553
|
+
}, {});
|
|
1554
|
+
|
|
1555
|
+
const invalidDefinitionsByAccessPattern = Object.entries(byAccessPattern).map(([accessPattern, attributes]) => {
|
|
1556
|
+
return `${accessPattern}: ${u.commaSeparatedString(attributes)}`
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
throw new e.ElectroError(
|
|
1560
|
+
e.ErrorCodes.InvalidProjectionDefinition,
|
|
1561
|
+
`Unknown index projection attributes provided. The following access patterns were defined with unknown attributes: ${u.commaSeparatedString(invalidDefinitionsByAccessPattern, '', '')}`,
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1543
1565
|
let missingFacetAttributes = Array.isArray(facets.attributes)
|
|
1544
1566
|
? facets.attributes
|
|
1545
1567
|
.filter(({ name }) => !normalized[name])
|
|
1546
1568
|
.map((facet) => `"${facet.type}: ${facet.name}"`)
|
|
1547
1569
|
: [];
|
|
1570
|
+
|
|
1548
1571
|
if (missingFacetAttributes.length > 0) {
|
|
1549
1572
|
throw new e.ElectroError(
|
|
1550
1573
|
e.ErrorCodes.InvalidKeyCompositeAttributeTemplate,
|
package/src/service.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
ElectroInstanceTypes,
|
|
14
14
|
ModelVersions,
|
|
15
15
|
IndexTypes,
|
|
16
|
-
DataOptions,
|
|
16
|
+
DataOptions, IndexProjectionOptions,
|
|
17
17
|
} = require("./types");
|
|
18
18
|
const { FilterFactory } = require("./filters");
|
|
19
19
|
const { FilterOperations } = require("./operations");
|
|
@@ -369,6 +369,7 @@ class Service {
|
|
|
369
369
|
record,
|
|
370
370
|
entities: this.entities,
|
|
371
371
|
allowMatchOnKeys: config.ignoreOwnership,
|
|
372
|
+
config,
|
|
372
373
|
});
|
|
373
374
|
|
|
374
375
|
if (!entityAlias) {
|
|
@@ -460,7 +461,7 @@ class Service {
|
|
|
460
461
|
const expression = identifiers.expression || "";
|
|
461
462
|
|
|
462
463
|
let options = {
|
|
463
|
-
// expressions, // DynamoDB
|
|
464
|
+
// expressions, // DynamoDB doesn't return what I expect it would when provided with these entity filters
|
|
464
465
|
parse: (options, data) => {
|
|
465
466
|
if (options.data === DataOptions.raw) {
|
|
466
467
|
return data;
|
|
@@ -497,29 +498,21 @@ class Service {
|
|
|
497
498
|
hydrator: undefined,
|
|
498
499
|
_isCollectionQuery: false,
|
|
499
500
|
ignoreOwnership: config._providedIgnoreOwnership,
|
|
501
|
+
attributes: config._providedAttributes,
|
|
500
502
|
});
|
|
501
503
|
}
|
|
502
504
|
|
|
503
|
-
// let itemLookup = [];
|
|
504
505
|
let entityItemRefs = {};
|
|
505
|
-
// let entityResultRefs = {};
|
|
506
506
|
for (let i = 0; i < items.length; i++) {
|
|
507
507
|
const item = items[i];
|
|
508
508
|
for (let entityName in entities) {
|
|
509
509
|
entityItemRefs[entityName] = entityItemRefs[entityName] || [];
|
|
510
510
|
const entity = entities[entityName];
|
|
511
|
-
|
|
512
|
-
if (entity.ownsKeys(item)) {
|
|
513
|
-
// const entityItemRefsIndex =
|
|
511
|
+
if (entity.is(item, config)) {
|
|
514
512
|
entityItemRefs[entityName].push({
|
|
515
513
|
item,
|
|
516
514
|
itemSlot: i,
|
|
517
515
|
});
|
|
518
|
-
// itemLookup[i] = {
|
|
519
|
-
// entityName,
|
|
520
|
-
// entityItemRefsIndex,
|
|
521
|
-
// originalItem: item,
|
|
522
|
-
// }
|
|
523
516
|
}
|
|
524
517
|
}
|
|
525
518
|
}
|
|
@@ -536,6 +529,7 @@ class Service {
|
|
|
536
529
|
hydrator: undefined,
|
|
537
530
|
_isCollectionQuery: false,
|
|
538
531
|
ignoreOwnership: config._providedIgnoreOwnership,
|
|
532
|
+
attributes: config._providedAttributes,
|
|
539
533
|
});
|
|
540
534
|
unprocessed = unprocessed.concat(results.unprocessed);
|
|
541
535
|
if (results.data.length !== itemRefs.length) {
|
|
@@ -575,6 +569,12 @@ class Service {
|
|
|
575
569
|
};
|
|
576
570
|
}
|
|
577
571
|
|
|
572
|
+
_validateIndexProjectionsMatch(definition = {}, providedIndex = {}) {
|
|
573
|
+
const definitionProjection = definition.projection;
|
|
574
|
+
const providedProjection = providedIndex.projection;
|
|
575
|
+
return v.isMatchingProjection(providedIndex.projection, definition.projection)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
578
|
_validateCollectionDefinition(definition = {}, providedIndex = {}) {
|
|
579
579
|
let isCustomMatchPK = definition.pk.isCustom === providedIndex.pk.isCustom;
|
|
580
580
|
let isCustomMatchSK =
|
|
@@ -594,6 +594,11 @@ class Service {
|
|
|
594
594
|
providedIndex,
|
|
595
595
|
);
|
|
596
596
|
|
|
597
|
+
const matchingProjection = v.isMatchingProjection(
|
|
598
|
+
providedIndex.projection,
|
|
599
|
+
definition.projection
|
|
600
|
+
)
|
|
601
|
+
|
|
597
602
|
for (
|
|
598
603
|
let i = 0;
|
|
599
604
|
i < Math.max(definition.pk.labels.length, providedIndex.pk.labels.length);
|
|
@@ -699,6 +704,12 @@ class Service {
|
|
|
699
704
|
);
|
|
700
705
|
}
|
|
701
706
|
|
|
707
|
+
if (!matchingProjection) {
|
|
708
|
+
collectionDifferences.push(
|
|
709
|
+
`The provided projection definition ${u.commaSeparatedString(providedIndex.projection ?? '<undefined>')} does not match the established projection definition ${u.commaSeparatedString(definition.projection)} on index ${providedIndexName}. Index projection definitions must match across all entities participating in a collection`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
702
713
|
if (!matchingKeyCasing.sk) {
|
|
703
714
|
const definedSk = definition.sk || {};
|
|
704
715
|
const providedSk = providedIndex.sk || {};
|
package/src/types.js
CHANGED
|
@@ -335,6 +335,18 @@ const CastKeyOptions = {
|
|
|
335
335
|
number: "number",
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
+
const IndexProjectionOptions = {
|
|
339
|
+
all: 'all',
|
|
340
|
+
keys_only: 'keys_only',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const EntityIdentifiers = {
|
|
344
|
+
entity: "__edb_e__",
|
|
345
|
+
version: "__edb_v__",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const EntityIdentifierFields = ["__edb_e__", "__edb_v__"];
|
|
349
|
+
|
|
338
350
|
module.exports = {
|
|
339
351
|
Pager,
|
|
340
352
|
KeyTypes,
|
|
@@ -381,4 +393,7 @@ module.exports = {
|
|
|
381
393
|
UpsertOperations,
|
|
382
394
|
BatchWriteTypes,
|
|
383
395
|
DefaultKeyCasing,
|
|
396
|
+
IndexProjectionOptions,
|
|
397
|
+
EntityIdentifiers,
|
|
398
|
+
EntityIdentifierFields,
|
|
384
399
|
};
|
package/src/util.js
CHANGED
|
@@ -79,9 +79,19 @@ function batchItems(arr = [], size) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function commaSeparatedString(array = [], prefix = '"', postfix = '"') {
|
|
82
|
+
if (typeof array === 'string') {
|
|
83
|
+
array = [array];
|
|
84
|
+
}
|
|
82
85
|
return array.map((value) => `${prefix}${value}${postfix}`).join(", ");
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
function toDisplayString(value) {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return "<undefined>";
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
function formatStringCasing(str, casing, defaultCase) {
|
|
86
96
|
if (typeof str !== "string") {
|
|
87
97
|
return str;
|
|
@@ -275,6 +285,7 @@ module.exports = {
|
|
|
275
285
|
removeFixings,
|
|
276
286
|
parseJSONPath,
|
|
277
287
|
shiftSortOrder,
|
|
288
|
+
toDisplayString,
|
|
278
289
|
getFirstDefined,
|
|
279
290
|
getInstanceType,
|
|
280
291
|
getModelVersion,
|
package/src/validations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const e = require("./errors");
|
|
2
|
-
const { KeyCasing } = require("./types");
|
|
2
|
+
const { KeyCasing, IndexProjectionOptions } = require("./types");
|
|
3
3
|
|
|
4
4
|
const Validator = require("jsonschema").Validator;
|
|
5
5
|
Validator.prototype.customFormats.isFunction = function (input) {
|
|
@@ -179,6 +179,12 @@ const Index = {
|
|
|
179
179
|
required: false,
|
|
180
180
|
format: "isFunction",
|
|
181
181
|
},
|
|
182
|
+
projection: {
|
|
183
|
+
type: ["array", "string"],
|
|
184
|
+
items: {
|
|
185
|
+
type: "string",
|
|
186
|
+
}
|
|
187
|
+
},
|
|
182
188
|
},
|
|
183
189
|
};
|
|
184
190
|
|
|
@@ -398,10 +404,25 @@ function isMatchingCasing(casing1, casing2) {
|
|
|
398
404
|
}
|
|
399
405
|
}
|
|
400
406
|
|
|
407
|
+
function isValueOrUndefined(received, expected) {
|
|
408
|
+
return expected === received || received === undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isMatchingProjection(received, expected) {
|
|
412
|
+
if (isStringHasLength(received) && isStringHasLength(expected)) {
|
|
413
|
+
return received === expected;
|
|
414
|
+
} else if (Array.isArray(received) && Array.isArray(expected)) {
|
|
415
|
+
return received.length === expected.length && expected.every((attribute) => received.includes(attribute));
|
|
416
|
+
} else {
|
|
417
|
+
return isValueOrUndefined(received, IndexProjectionOptions.all) && isValueOrUndefined(expected, IndexProjectionOptions.all)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
401
421
|
module.exports = {
|
|
402
422
|
testModel,
|
|
403
423
|
isFunction,
|
|
404
424
|
stringArrayMatch,
|
|
425
|
+
isMatchingProjection,
|
|
405
426
|
isMatchingCasing,
|
|
406
427
|
isArrayHasLength,
|
|
407
428
|
isStringHasLength,
|