electrodb 1.8.2 → 1.9.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/README.md +23 -19
- package/index.d.ts +15 -5
- package/package.json +1 -1
- package/src/entity.js +91 -34
- package/src/errors.js +6 -0
- package/src/util.js +36 -3
- package/src/validations.js +0 -2
- package/CHANGELOG.md +0 -173
package/README.md
CHANGED
|
@@ -1071,7 +1071,7 @@ When using ElectroDB, indexes are referenced by their `AccessPatternName`. This
|
|
|
1071
1071
|
|
|
1072
1072
|
All DynamoDB table start with at least a PartitionKey with an optional SortKey, this can be referred to as the _"Table Index"_. The `indexes` object requires at least the definition of this _Table Index_ **Partition Key** and (if applicable) **Sort Key**.
|
|
1073
1073
|
|
|
1074
|
-
In your model, the _Table Index_ this is expressed as an _Access Pattern_ *without* an `index` property. For Secondary Indexes, use the `index` property to define the name of the index as defined on your DynamoDB table.
|
|
1074
|
+
In your model, the _Table Index_ this is expressed as an _Access Pattern_ *without* an `index` property. For Secondary Indexes (both GSIs and LSIs), use the `index` property to define the name of the index as defined on your DynamoDB table.
|
|
1075
1075
|
|
|
1076
1076
|
Within these _AccessPatterns_, you define the PartitionKey and (optionally) SortKeys that are present on your DynamoDB table and map the key's name on the table with the `field` property.
|
|
1077
1077
|
|
|
@@ -2739,7 +2739,7 @@ const MallStore = new Entity(schema, {table: "StoreDirectory"});
|
|
|
2739
2739
|
#### Partition Key Composite Attributes
|
|
2740
2740
|
All queries require (*at minimum*) the **Composite Attributes** included in its defined **Partition Key**. **Composite Attributes** you define on the **Sort Key** can be partially supplied, but must be supplied in the order they are defined.
|
|
2741
2741
|
|
|
2742
|
-
> *
|
|
2742
|
+
> *IMPORTANT: Composite Attributes must be supplied in the order they are composed when invoking the **Access Pattern***. This is because composite attributes are used to form a concatenated key string, and if attributes supplied out of order, it is not possible to fill the gaps in that concatenation.
|
|
2743
2743
|
|
|
2744
2744
|
```javascript
|
|
2745
2745
|
const MallStore = new Entity({
|
|
@@ -2908,6 +2908,8 @@ The two-dimensional array returned by batch get most easily used when deconstruc
|
|
|
2908
2908
|
|
|
2909
2909
|
The `results` array are records that were returned DynamoDB as `Responses` on the BatchGet query. They will appear in the same format as other ElectroDB queries.
|
|
2910
2910
|
|
|
2911
|
+
> _NOTE: By default ElectroDB will return items without concern for order. If the order returned by ElectroDB must match the order provided, the [query option](#query-options) `preserveBatchOrder` can be used. When enabled, ElectroDB will ensure the order returned by a batchGet will be the same as the order provided. When enabled, if a record is returned from DynamoDB as "unprocessed" ([read more here](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)), ElectroDB will return a null value at that index._
|
|
2912
|
+
|
|
2911
2913
|
Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [query option](#query-options) `{unprocessed:"raw"}` override this behavior and return the Keys as they came from DynamoDB.
|
|
2912
2914
|
|
|
2913
2915
|
### Delete Method
|
|
@@ -3245,7 +3247,7 @@ entity.update({ attr1: "value1", attr2: "value2" })
|
|
|
3245
3247
|
}
|
|
3246
3248
|
```
|
|
3247
3249
|
|
|
3248
|
-
>
|
|
3250
|
+
> _NOTE: Included in the update are all attributes from the table's primary index. These values are automatically included on all updates in the event an update results in an insert.__
|
|
3249
3251
|
|
|
3250
3252
|
#### Update Method: Set
|
|
3251
3253
|
|
|
@@ -4297,25 +4299,27 @@ By default, **ElectroDB** enables you to work with records as the names and prop
|
|
|
4297
4299
|
pages?: number;
|
|
4298
4300
|
logger?: (event) => void;
|
|
4299
4301
|
listeners Array<(event) => void>;
|
|
4302
|
+
preserveBatchOrder?: boolean;
|
|
4300
4303
|
};
|
|
4301
4304
|
```
|
|
4302
4305
|
|
|
4303
|
-
Option
|
|
4304
|
-
|
|
4305
|
-
params
|
|
4306
|
-
table
|
|
4307
|
-
raw
|
|
4308
|
-
includeKeys
|
|
4309
|
-
pager
|
|
4310
|
-
originalErr
|
|
4311
|
-
concurrent
|
|
4312
|
-
unprocessed
|
|
4313
|
-
response
|
|
4314
|
-
ignoreOwnership
|
|
4315
|
-
limit
|
|
4316
|
-
pages
|
|
4317
|
-
listeners
|
|
4318
|
-
logger
|
|
4306
|
+
Option | Default | Description
|
|
4307
|
+
------------------ | :------------------: | -----------
|
|
4308
|
+
params | `{}` | Properties added to this object will be merged onto the params sent to the document client. Any conflicts with **ElectroDB** will favor the params specified here.
|
|
4309
|
+
table | _(from constructor)_ | Use a different table than the one defined in the [Service Options](#service-options)
|
|
4310
|
+
raw | `false` | Returns query results as they were returned by the docClient.
|
|
4311
|
+
includeKeys | `false` | By default, **ElectroDB** does not return partition, sort, or global keys in its response.
|
|
4312
|
+
pager | `"named"` | Used in with pagination (`.pages()`) calls to override ElectroDBs default behaviour to break apart `LastEvaluatedKeys` records into composite attributes. See more detail about this in the sections for [Pager Query Options](#pager-query-options).
|
|
4313
|
+
originalErr | `false` | By default, **ElectroDB** alters the stacktrace of any exceptions thrown by the DynamoDB client to give better visibility to the developer. Set this value equal to `true` to turn off this functionality and return the error unchanged.
|
|
4314
|
+
concurrent | `1` | When performing batch operations, how many requests (1 batch operation == 1 request) to DynamoDB should ElectroDB make at one time. Be mindful of your DynamoDB throughput configurations
|
|
4315
|
+
unprocessed | `"item"` | Used in batch processing to override ElectroDBs default behaviour to break apart DynamoDBs `Unprocessed` records into composite attributes. See more detail about this in the sections for [BatchGet](#batch-get), [BatchDelete](#batch-write-delete-records), and [BatchPut](#batch-write-put-records).
|
|
4316
|
+
response | `"default"` | Used as a convenience for applying the DynamoDB parameter `ReturnValues`. The options here are the same as the parameter values for the DocumentClient except lowercase. The `"none"` option will cause the method to return null and will bypass ElectroDB's response formatting -- useful if formatting performance is a concern.
|
|
4317
|
+
ignoreOwnership | `false` | By default, **ElectroDB** interrogates items returned from a query for the presence of matching entity "identifiers". This helps to ensure other entities, or other versions of an entity, are filtered from your results. If you are using ElectroDB with an existing table/dataset you can turn off this feature by setting this property to `true`.
|
|
4318
|
+
limit | _none_ | A target for the number of items to return from DynamoDB. If this option is passed, Queries on entities and through collections will paginate DynamoDB until this limit is reached or all items for that query have been returned.
|
|
4319
|
+
pages | ∞ | How many DynamoDB pages should a query iterate through before stopping. By default ElectroDB paginate through all results for your query.
|
|
4320
|
+
listeners | `[]` | An array of callbacks that are invoked when [internal ElectroDB events](#events) occur.
|
|
4321
|
+
logger | _none_ | A convenience option for a single event listener that semantically can be used for logging.
|
|
4322
|
+
preserveBatchOrder | `false` | When used with a [batchGet](#batch-get) operation, ElectroDB will ensure the order returned by a batchGet will be the same as the order provided. When enabled, if a record is returned from DynamoDB as "unprocessed" ([read more here](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)), ElectroDB will return a null value at that index.
|
|
4319
4323
|
|
|
4320
4324
|
# AWS DynamoDB Client
|
|
4321
4325
|
ElectroDB supports both the [v2](https://www.npmjs.com/package/aws-sdk) and [v3](https://www.npmjs.com/package/@aws-sdk/client-dynamodb) aws clients. The client can be supplied creating a new Entity or Service, or added to a Entity/Service instance via the `setClient()` method.
|
package/index.d.ts
CHANGED
|
@@ -1023,6 +1023,7 @@ interface PaginationOptions extends QueryOptions {
|
|
|
1023
1023
|
interface BulkOptions extends QueryOptions {
|
|
1024
1024
|
unprocessed?: "raw" | "item";
|
|
1025
1025
|
concurrency?: number;
|
|
1026
|
+
preserveBatchOrder?: boolean;
|
|
1026
1027
|
}
|
|
1027
1028
|
|
|
1028
1029
|
type OptionalDefaultEntityIdentifiers = {
|
|
@@ -1032,6 +1033,15 @@ type OptionalDefaultEntityIdentifiers = {
|
|
|
1032
1033
|
|
|
1033
1034
|
type GoRecord<ResponseType, Options = QueryOptions> = <T = ResponseType>(options?: Options) => Promise<T>;
|
|
1034
1035
|
|
|
1036
|
+
type BatchGoRecord<ResponseType, AlternateResponseType> = <O extends BulkOptions>(options?: O) =>
|
|
1037
|
+
O extends infer Options
|
|
1038
|
+
? 'preserveBatchOrder' extends keyof Options
|
|
1039
|
+
? Options['preserveBatchOrder'] extends true
|
|
1040
|
+
? Promise<AlternateResponseType>
|
|
1041
|
+
: Promise<ResponseType>
|
|
1042
|
+
: Promise<ResponseType>
|
|
1043
|
+
: never
|
|
1044
|
+
|
|
1035
1045
|
type PageRecord<ResponseType, CompositeAttributes> = (page?: (CompositeAttributes & OptionalDefaultEntityIdentifiers) | null, options?: PaginationOptions) => Promise<[
|
|
1036
1046
|
(CompositeAttributes & OptionalDefaultEntityIdentifiers) | null,
|
|
1037
1047
|
ResponseType
|
|
@@ -1070,8 +1080,8 @@ type DeleteRecordOperationOptions<A extends string, F extends A, C extends strin
|
|
|
1070
1080
|
where: WhereClause<A,F,C,S,Item<A,F,C,S,S["attributes"]>,DeleteRecordOperationOptions<A,F,C,S,ResponseType>>;
|
|
1071
1081
|
};
|
|
1072
1082
|
|
|
1073
|
-
type BulkRecordOperationOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, ResponseType> = {
|
|
1074
|
-
go:
|
|
1083
|
+
type BulkRecordOperationOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, ResponseType, AlternateResponseType> = {
|
|
1084
|
+
go: BatchGoRecord<ResponseType, AlternateResponseType>;
|
|
1075
1085
|
params: ParamRecord<BulkOptions>;
|
|
1076
1086
|
};
|
|
1077
1087
|
|
|
@@ -1221,9 +1231,9 @@ export class Entity<A extends string, F extends A, C extends string, S extends S
|
|
|
1221
1231
|
readonly schema: S;
|
|
1222
1232
|
constructor(schema: S, config?: EntityConfiguration);
|
|
1223
1233
|
get(key: AllTableIndexCompositeAttributes<A,F,C,S>): SingleRecordOperationOptions<A,F,C,S, ResponseItem<A,F,C,S> | null>;
|
|
1224
|
-
get(key: AllTableIndexCompositeAttributes<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, [Array<ResponseItem<A,F,C,S
|
|
1234
|
+
get(key: AllTableIndexCompositeAttributes<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, [Array<Flatten<ResponseItem<A,F,C,S>>>, Array<Flatten<AllTableIndexCompositeAttributes<A,F,C,S>>>], [Array<Flatten<ResponseItem<A,F,C,S>> | null>, Array<Flatten<AllTableIndexCompositeAttributes<A,F,C,S>>>]>;
|
|
1225
1235
|
delete(key: AllTableIndexCompositeAttributes<A,F,C,S>): DeleteRecordOperationOptions<A,F,C,S, ResponseItem<A,F,C,S>>;
|
|
1226
|
-
delete(key: AllTableIndexCompositeAttributes<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, AllTableIndexCompositeAttributes<A,F,C,S>[]>;
|
|
1236
|
+
delete(key: AllTableIndexCompositeAttributes<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, AllTableIndexCompositeAttributes<A,F,C,S>[], AllTableIndexCompositeAttributes<A,F,C,S>[]>;
|
|
1227
1237
|
remove(key: AllTableIndexCompositeAttributes<A,F,C,S>): DeleteRecordOperationOptions<A,F,C,S, ResponseItem<A,F,C,S>>;
|
|
1228
1238
|
update(key: AllTableIndexCompositeAttributes<A,F,C,S>): {
|
|
1229
1239
|
set: SetRecord<A,F,C,S, SetItem<A,F,C,S>, TableIndexCompositeAttributes<A,F,C,S>, ResponseItem<A,F,C,S>>;
|
|
@@ -1244,7 +1254,7 @@ export class Entity<A extends string, F extends A, C extends string, S extends S
|
|
|
1244
1254
|
data: DataUpdateMethodRecord<A,F,C,S, Item<A,F,C,S,S["attributes"]>, TableIndexCompositeAttributes<A,F,C,S>, ResponseItem<A,F,C,S>>;
|
|
1245
1255
|
};
|
|
1246
1256
|
put(record: PutItem<A,F,C,S>): PutRecordOperationOptions<A,F,C,S, ResponseItem<A,F,C,S>>;
|
|
1247
|
-
put(record: PutItem<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, AllTableIndexCompositeAttributes<A,F,C,S>[]>;
|
|
1257
|
+
put(record: PutItem<A,F,C,S>[]): BulkRecordOperationOptions<A,F,C,S, AllTableIndexCompositeAttributes<A,F,C,S>[], AllTableIndexCompositeAttributes<A,F,C,S>[]>;
|
|
1248
1258
|
create(record: PutItem<A,F,C,S>): PutRecordOperationOptions<A,F,C,S, ResponseItem<A,F,C,S>>;
|
|
1249
1259
|
find(record: Partial<Item<A,F,C,S,S["attributes"]>>): RecordsActionOptions<A,F,C,S, ResponseItem<A,F,C,S>[], AllTableIndexCompositeAttributes<A,F,C,S>>;
|
|
1250
1260
|
match(record: Partial<Item<A,F,C,S,S["attributes"]>>): RecordsActionOptions<A,F,C,S, ResponseItem<A,F,C,S>[], AllTableIndexCompositeAttributes<A,F,C,S>>;
|
package/package.json
CHANGED
package/src/entity.js
CHANGED
|
@@ -277,6 +277,7 @@ class Entity {
|
|
|
277
277
|
results,
|
|
278
278
|
}, config.listeners);
|
|
279
279
|
}
|
|
280
|
+
|
|
280
281
|
return this.client[method](params).promise()
|
|
281
282
|
.then((results) => {
|
|
282
283
|
notifyQuery();
|
|
@@ -317,14 +318,36 @@ class Entity {
|
|
|
317
318
|
return results;
|
|
318
319
|
}
|
|
319
320
|
|
|
321
|
+
_createNewBatchGetOrderMaintainer(config = {}) {
|
|
322
|
+
const pkName = this.model.translations.keys[TableIndex].pk;
|
|
323
|
+
const skName = this.model.translations.keys[TableIndex].sk;
|
|
324
|
+
const enabled = !!config.preserveBatchOrder;
|
|
325
|
+
const table = this.config.table;
|
|
326
|
+
const keyFormatter = ((record = {}) => {
|
|
327
|
+
const pk = record[pkName];
|
|
328
|
+
const sk = record[skName];
|
|
329
|
+
return `${pk}${sk}`;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return new u.BatchGetOrderMaintainer({
|
|
333
|
+
table,
|
|
334
|
+
enabled,
|
|
335
|
+
keyFormatter,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
320
339
|
async executeBulkGet(parameters, config) {
|
|
321
340
|
if (!Array.isArray(parameters)) {
|
|
322
341
|
parameters = [parameters];
|
|
323
342
|
}
|
|
324
|
-
let concurrent = this._normalizeConcurrencyValue(config.concurrent)
|
|
325
|
-
let concurrentOperations = u.batchItems(parameters, concurrent);
|
|
326
343
|
|
|
327
|
-
|
|
344
|
+
const orderMaintainer = this._createNewBatchGetOrderMaintainer(config);
|
|
345
|
+
orderMaintainer.defineOrder(parameters);
|
|
346
|
+
let concurrent = this._normalizeConcurrencyValue(config.concurrent);
|
|
347
|
+
let concurrentOperations = u.batchItems(parameters, concurrent);
|
|
348
|
+
let resultsAll = config.preserveBatchOrder
|
|
349
|
+
? new Array(orderMaintainer.getSize()).fill(null)
|
|
350
|
+
: [];
|
|
328
351
|
let unprocessedAll = [];
|
|
329
352
|
for (let operation of concurrentOperations) {
|
|
330
353
|
await Promise.all(operation.map(async params => {
|
|
@@ -333,13 +356,13 @@ class Entity {
|
|
|
333
356
|
resultsAll.push(await config.parse(config, response));
|
|
334
357
|
return;
|
|
335
358
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
resultsAll
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
359
|
+
this.applyBulkGetResponseFormatting({
|
|
360
|
+
orderMaintainer,
|
|
361
|
+
resultsAll,
|
|
362
|
+
unprocessedAll,
|
|
363
|
+
response,
|
|
364
|
+
config
|
|
365
|
+
});
|
|
343
366
|
}));
|
|
344
367
|
}
|
|
345
368
|
return [resultsAll, unprocessedAll];
|
|
@@ -467,20 +490,26 @@ class Entity {
|
|
|
467
490
|
}
|
|
468
491
|
}
|
|
469
492
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
493
|
+
applyBulkGetResponseFormatting({
|
|
494
|
+
resultsAll,
|
|
495
|
+
unprocessedAll,
|
|
496
|
+
orderMaintainer,
|
|
497
|
+
response = {},
|
|
498
|
+
config = {},
|
|
499
|
+
}) {
|
|
473
500
|
const table = config.table || this._getTableName();
|
|
474
501
|
const index = TableIndex;
|
|
502
|
+
|
|
475
503
|
if (!response.UnprocessedKeys || !response.Responses) {
|
|
476
504
|
throw new Error("Unknown response format");
|
|
477
505
|
}
|
|
506
|
+
|
|
478
507
|
if (response.UnprocessedKeys[table] && response.UnprocessedKeys[table].Keys && Array.isArray(response.UnprocessedKeys[table].Keys)) {
|
|
479
508
|
for (let value of response.UnprocessedKeys[table].Keys) {
|
|
480
509
|
if (config && config.unprocessed === UnprocessedTypes.raw) {
|
|
481
|
-
|
|
510
|
+
unprocessedAll.push(value);
|
|
482
511
|
} else {
|
|
483
|
-
|
|
512
|
+
unprocessedAll.push(
|
|
484
513
|
this._formatKeysToItem(index, value)
|
|
485
514
|
);
|
|
486
515
|
}
|
|
@@ -488,10 +517,18 @@ class Entity {
|
|
|
488
517
|
}
|
|
489
518
|
|
|
490
519
|
if (response.Responses[table] && Array.isArray(response.Responses[table])) {
|
|
491
|
-
|
|
520
|
+
const responses = response.Responses[table];
|
|
521
|
+
for (let i = 0; i < responses.length; i++) {
|
|
522
|
+
const item = responses[i];
|
|
523
|
+
const slot = orderMaintainer.getOrder(item);
|
|
524
|
+
const formatted = this.formatResponse({Item: item}, index, config);
|
|
525
|
+
if (slot !== -1) {
|
|
526
|
+
resultsAll[slot] = formatted;
|
|
527
|
+
} else {
|
|
528
|
+
resultsAll.push(formatted);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
492
531
|
}
|
|
493
|
-
|
|
494
|
-
return [results, unprocessed];
|
|
495
532
|
}
|
|
496
533
|
|
|
497
534
|
formatResponse(response, index, config = {}) {
|
|
@@ -782,6 +819,7 @@ class Entity {
|
|
|
782
819
|
_isCollectionQuery: false,
|
|
783
820
|
pages: undefined,
|
|
784
821
|
listeners: [],
|
|
822
|
+
preserveBatchOrder: false,
|
|
785
823
|
};
|
|
786
824
|
|
|
787
825
|
config = options.reduce((config, option) => {
|
|
@@ -794,6 +832,10 @@ class Entity {
|
|
|
794
832
|
config.params.ReturnValues = FormatToReturnValues[format];
|
|
795
833
|
}
|
|
796
834
|
|
|
835
|
+
if (option.preserveBatchOrder === true) {
|
|
836
|
+
config.preserveBatchOrder = true;
|
|
837
|
+
}
|
|
838
|
+
|
|
797
839
|
if (option.pages !== undefined) {
|
|
798
840
|
config.pages = option.pages;
|
|
799
841
|
}
|
|
@@ -2257,22 +2299,6 @@ class Entity {
|
|
|
2257
2299
|
facets.fields.push(sk.field);
|
|
2258
2300
|
}
|
|
2259
2301
|
|
|
2260
|
-
if (seenIndexFields[pk.field] !== undefined) {
|
|
2261
|
-
throw new e.ElectroError(e.ErrorCodes.DuplicateIndexFields, `Partition Key (pk) on Access Pattern '${accessPattern}' references the field '${pk.field}' which is already referenced by the Access Pattern '${seenIndexFields[pk.field]}'. Fields used for indexes need to be unique to avoid conflicts.`);
|
|
2262
|
-
} else {
|
|
2263
|
-
seenIndexFields[pk.field] = accessPattern;
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
if (sk.field) {
|
|
2267
|
-
if (sk.field === pk.field) {
|
|
2268
|
-
throw new e.ElectroError(e.ErrorCodes.DuplicateIndexFields, `The Access Pattern '${accessPattern}' references the field '${sk.field}' as the field name for both the PK and SK. Fields used for indexes need to be unique to avoid conflicts.`);
|
|
2269
|
-
} else if (seenIndexFields[sk.field] !== undefined) {
|
|
2270
|
-
throw new e.ElectroError(e.ErrorCodes.DuplicateIndexFields, `Sort Key (sk) on Access Pattern '${accessPattern}' references the field '${sk.field}' which is already referenced by the Access Pattern '${seenIndexFields[sk.field]}'. Fields used for indexes need to be unique to avoid conflicts.`);
|
|
2271
|
-
}else {
|
|
2272
|
-
seenIndexFields[sk.field] = accessPattern;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
2302
|
if (Array.isArray(sk.facets)) {
|
|
2277
2303
|
let duplicates = pk.facets.filter(facet => sk.facets.includes(facet));
|
|
2278
2304
|
if (duplicates.length !== 0) {
|
|
@@ -2362,6 +2388,37 @@ class Entity {
|
|
|
2362
2388
|
facets.byField[sk.field][indexName] = sk;
|
|
2363
2389
|
}
|
|
2364
2390
|
|
|
2391
|
+
if (seenIndexFields[pk.field] !== undefined) {
|
|
2392
|
+
const definition = Object.values(facets.byField[pk.field]).find(definition => definition.index !== indexName)
|
|
2393
|
+
const definitionsMatch = validations.stringArrayMatch(pk.facets, definition.facets);
|
|
2394
|
+
if (!definitionsMatch) {
|
|
2395
|
+
throw new e.ElectroError(e.ErrorCodes.InconsistentIndexDefinition, `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(accessPattern)}' is defined with the composite attribute(s) ${u.commaSeparatedString(pk.facets)}, but the accessPattern '${u.formatIndexNameForDisplay(definition.index)}' defines this field with the composite attributes ${u.commaSeparatedString(definition.facets)}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`);
|
|
2396
|
+
}
|
|
2397
|
+
seenIndexFields[pk.field].push({accessPattern, type: 'pk'});
|
|
2398
|
+
} else {
|
|
2399
|
+
seenIndexFields[pk.field] = [];
|
|
2400
|
+
seenIndexFields[pk.field].push({accessPattern, type: 'pk'});
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
if (sk.field) {
|
|
2404
|
+
if (sk.field === pk.field) {
|
|
2405
|
+
throw new e.ElectroError(e.ErrorCodes.DuplicateIndexFields, `The Access Pattern '${u.formatIndexNameForDisplay(accessPattern)}' references the field '${sk.field}' as the field name for both the PK and SK. Fields used for indexes need to be unique to avoid conflicts.`);
|
|
2406
|
+
} else if (seenIndexFields[sk.field] !== undefined) {
|
|
2407
|
+
const isAlsoDefinedAsPK = seenIndexFields[sk.field].find(field => field.type === "pk");
|
|
2408
|
+
if (isAlsoDefinedAsPK) {
|
|
2409
|
+
throw new e.ElectroError(e.ErrorCodes.InconsistentIndexDefinition, `The Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(accessPattern)}' references the field '${pk.field}' which is already referenced by the Access Pattern(s) '${u.formatIndexNameForDisplay(isAlsoDefinedAsPK.accessPattern)}' as a Partition Key. Fields mapped to Partition Keys cannot be also mapped to Sort Keys.`);
|
|
2410
|
+
}
|
|
2411
|
+
const definition = Object.values(facets.byField[sk.field]).find(definition => definition.index !== indexName)
|
|
2412
|
+
const definitionsMatch = validations.stringArrayMatch(sk.facets, definition.facets);
|
|
2413
|
+
if (!definitionsMatch) {
|
|
2414
|
+
throw new e.ElectroError(e.ErrorCodes.DuplicateIndexFields, `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(accessPattern)}' is defined with the composite attribute(s) ${u.commaSeparatedString(sk.facets)}, but the accessPattern '${u.formatIndexNameForDisplay(definition.index)}' defines this field with the composite attributes ${u.commaSeparatedString(definition.facets)}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`);
|
|
2415
|
+
}
|
|
2416
|
+
seenIndexFields[sk.field].push({accessPattern, type: 'sk'});
|
|
2417
|
+
} else {
|
|
2418
|
+
seenIndexFields[sk.field] = [];
|
|
2419
|
+
seenIndexFields[sk.field].push({accessPattern, type: 'sk'});
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2365
2422
|
|
|
2366
2423
|
attributes.forEach(({index, type, name}, j) => {
|
|
2367
2424
|
let next = attributes[j + 1] !== undefined ? attributes[j + 1].name : "";
|
package/src/errors.js
CHANGED
|
@@ -139,6 +139,12 @@ const ErrorCodes = {
|
|
|
139
139
|
name: "InvalidClientProvided",
|
|
140
140
|
sym: ErrorCode,
|
|
141
141
|
},
|
|
142
|
+
InconsistentIndexDefinition: {
|
|
143
|
+
code: 1022,
|
|
144
|
+
section: "inconsistent-index-definition",
|
|
145
|
+
name: "InvalidClientProvided",
|
|
146
|
+
sym: ErrorCode,
|
|
147
|
+
},
|
|
142
148
|
MissingAttribute: {
|
|
143
149
|
code: 2001,
|
|
144
150
|
section: "missing-attribute",
|
package/src/util.js
CHANGED
|
@@ -75,8 +75,8 @@ function batchItems(arr = [], size) {
|
|
|
75
75
|
return batched;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function commaSeparatedString(array = []) {
|
|
79
|
-
return array.map(value =>
|
|
78
|
+
function commaSeparatedString(array = [], prefix = '"', postfix = '"') {
|
|
79
|
+
return array.map(value => `${prefix}${value}${postfix}`).join(", ");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function formatStringCasing(str, casing, defaultCase) {
|
|
@@ -118,6 +118,38 @@ function formatIndexNameForDisplay(index) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
class BatchGetOrderMaintainer {
|
|
122
|
+
constructor({ table, enabled, keyFormatter }) {
|
|
123
|
+
this.table = table;
|
|
124
|
+
this.enabled = enabled;
|
|
125
|
+
this.keyFormatter = keyFormatter;
|
|
126
|
+
this.batchIndexMap = new Map();
|
|
127
|
+
this.currentSlot = 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getSize() {
|
|
131
|
+
return this.batchIndexMap.size;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getOrder(item) {
|
|
135
|
+
const key = this.keyFormatter(item);
|
|
136
|
+
return this.batchIndexMap.get(key) ?? -1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
defineOrder(parameters = []) {
|
|
140
|
+
if (this.enabled) {
|
|
141
|
+
for (let i = 0; i < parameters.length; i++) {
|
|
142
|
+
const batchParams = parameters[i];
|
|
143
|
+
const recordKeys = batchParams?.RequestItems?.[this.table]?.Keys ?? [];
|
|
144
|
+
for (const recordKey of recordKeys) {
|
|
145
|
+
const indexMapKey = this.keyFormatter(recordKey);
|
|
146
|
+
this.batchIndexMap.set(indexMapKey, this.currentSlot++);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
module.exports = {
|
|
122
154
|
batchItems,
|
|
123
155
|
parseJSONPath,
|
|
@@ -128,5 +160,6 @@ module.exports = {
|
|
|
128
160
|
commaSeparatedString,
|
|
129
161
|
formatAttributeCasing,
|
|
130
162
|
applyBetaModelOverrides,
|
|
131
|
-
formatIndexNameForDisplay
|
|
163
|
+
formatIndexNameForDisplay,
|
|
164
|
+
BatchGetOrderMaintainer,
|
|
132
165
|
};
|
package/src/validations.js
CHANGED
|
@@ -81,7 +81,6 @@ const Index = {
|
|
|
81
81
|
},
|
|
82
82
|
facets: {
|
|
83
83
|
type: ["array", "string"],
|
|
84
|
-
minItems: 1,
|
|
85
84
|
items: {
|
|
86
85
|
type: "string",
|
|
87
86
|
},
|
|
@@ -89,7 +88,6 @@ const Index = {
|
|
|
89
88
|
},
|
|
90
89
|
composite: {
|
|
91
90
|
type: ["array"],
|
|
92
|
-
minItems: 1,
|
|
93
91
|
items: {
|
|
94
92
|
type: "string",
|
|
95
93
|
},
|
package/CHANGELOG.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
All notable changes to this project will be documented in this file. Breaking changes to feature functionality will trigger a new Major Version increase. Significant feature improvements and major bug fixes will trigger Minor Version increases. Small, maintenance and additive changes will trigger Patch Version increases.
|
|
3
|
-
|
|
4
|
-
## [Unreleased]
|
|
5
|
-
## Changing
|
|
6
|
-
- Bulk Operations to return the original object passed to the operation if that object was returned by DynamoDB as unprocessed.
|
|
7
|
-
|
|
8
|
-
## [1.0.0] - 2021-06-27
|
|
9
|
-
### Added
|
|
10
|
-
- new `.match()` method to replace original Find method functionality. [[read more]](./README.md#match-records)
|
|
11
|
-
- New `template` property on Model for building custom composite key templates. The `template` property also brings forward a new syntax similar to template literal syntax. [[read more]](./README.md#composite-attribute-templates)
|
|
12
|
-
- New custom composite key syntax using the `template` property. [[read more]](./README.md#composite-attribute-templates)
|
|
13
|
-
- Numeric table keys now possible. Before PK and SK values could only be strings, Numeric Keys are now supported through the `templates` index property. [[read more]](./README.md#numeric-keys)
|
|
14
|
-
|
|
15
|
-
### Changed
|
|
16
|
-
- Rename of `facets` property on Model to `composite` for arrays and `template` for string templates. [[read more]](./README.md#the-renaming-of-index-property-facets-to-composite-and-template)
|
|
17
|
-
- Get method now returns `null` when value is not found. Prior functionality returned an empty object. [[read more]](./README.md#get-record)
|
|
18
|
-
- Strict enforcement of all SK composite attributes being present when performing `.get()`, `.put()`, `.create()`, `.delete()`, `.remove()`, `.update()`, `.patch()` operations.
|
|
19
|
-
- Find method now does not add filters for values supplied, Find now only identifies an Index (if possible) and fulfills the Composite Attributes of that Index (if possible). [[read more]](./README.md#find-records)
|
|
20
|
-
- Query Option `lastEvaluatedKeyRaw` when used with _pagination_ replaced Query Option `pager` with option values: `"raw"`, `"item"`, `"named"`. Default set to `"named"`. [[read more]](./README.md#pager-query-options)
|
|
21
|
-
- Query Option `lastEvaluatedKeyRaw` when used with _bulk operations_ replaced Query Option `unprocessed` with option values: `"raw"`, `"item"`. Default set to `"item"`. [[read more]](./README.md#query-options)
|
|
22
|
-
|
|
23
|
-
### Deprecated
|
|
24
|
-
- Removing `facets` property from documentation, examples, and TypeScript typing. Replaced with `composite` property for arrays and `template` for string templates. [[read more]](./README.md#facets)
|
|
25
|
-
- Fully deprecated custom facet string format. Facet strings defined attributes with a prefixed `:` as in `:storeId` would resolve to `storeId`. This has been replaced by the `template` syntax, surrounding the attribute with `${...}`. [[read more]](./README.md#composite-attribute-templates)
|
|
26
|
-
|
|
27
|
-
## [1.1.0] - 2021-07-07
|
|
28
|
-
### Added
|
|
29
|
-
- Expanding "collection" concept to include sub-collections. Sub-collections will allow for more precise cross-entity queries to be modeled. [[read more]](./README.md#sub-collections)
|
|
30
|
-
|
|
31
|
-
### Fixed
|
|
32
|
-
- Addressed edge-case when modeling sparse indexes that would leave unable to be queried via secondary index. [[read more]](./RELEASE.md#fix-sparse-index-edge-case)
|
|
33
|
-
|
|
34
|
-
## [1.1.1] - 2021-07-07
|
|
35
|
-
### Added
|
|
36
|
-
- Added new syntax for Attribute Property `watch` to trigger whenever any attribute is updated/retrieved. [[read more]](./README.md#attribute-watching-watch-all)
|
|
37
|
-
|
|
38
|
-
### Changed
|
|
39
|
-
- The Attribute Property `readOnly` is now enforced _before_ `watch` properties are evaluated. This allows properties that use the Attribute Property `watch` to deliberately circumnavigate `readOnly` enforcement. [[read more]](./README.md#createdat-and-updatedat-attributes)
|
|
40
|
-
|
|
41
|
-
## [1.2.0] - 2021-07-31
|
|
42
|
-
### Added
|
|
43
|
-
- Added new update methods `append`, `add`, `subtract`, `data`, `remove`, `delete`, and `data` for improved support of all DynamoDB update methods. [[read more]](./README.md#update-record)
|
|
44
|
-
|
|
45
|
-
### Changed
|
|
46
|
-
- The property names of `ExpressionAttributeValues` underwent some change in this release due to the addition of new update operations. This is not a breaking change but if you have tests to match on the exact params returned from ElectroDB these will likely break. [[read more]](./RELEASE.md#expressionattributevalues-properties)
|
|
47
|
-
|
|
48
|
-
## [1.3.0] - 2021-08-09
|
|
49
|
-
### Added
|
|
50
|
-
- New Attribute types `map`, `list`, `set`. [[read more]](./README.md#expanded-syntax)
|
|
51
|
-
- New Query Options, and support for, `ReturnValues` as requested in Issue#71. [[read more]](./README.md#query-options)
|
|
52
|
-
- New type definitions for recently released update methods `append`, `add`, `subtract`, `data`, `remove`, and `delete`. [[read more]](./README.md#exported-types)
|
|
53
|
-
|
|
54
|
-
### Changed
|
|
55
|
-
- Attributes that have been flagged as `required` are now not possible to be removed (using the update method `remove()`) from a stored Item. This was an oversight from the last release.
|
|
56
|
-
- Attributes that have been flagged as `hidden` now skips invoking that attribute's getter method.
|
|
57
|
-
|
|
58
|
-
### Fixed
|
|
59
|
-
- Issues that prevented the nesting of update `value()` operation.
|
|
60
|
-
- TypeScript type definitions for `get()` method now incorporate potential for `null` response.
|
|
61
|
-
- Type definitions for `value()` and `name()` where clause operations.
|
|
62
|
-
|
|
63
|
-
## [1.3.1] - 2021-08-09
|
|
64
|
-
### Added
|
|
65
|
-
- New entity method `parse()` to expose ElectroDB formatting for values retrieved outside of ElectroDB. [[read more]](./README.md#parse)
|
|
66
|
-
|
|
67
|
-
## [1.3.2] - 2021-08-11
|
|
68
|
-
### Fixed
|
|
69
|
-
- Newly added method `parse()` had critical typo. Method now has an improved api, and appropriate tests [[read more]](./README.md#parse)
|
|
70
|
-
|
|
71
|
-
## [1.4.0] - 2021-08-22
|
|
72
|
-
### Added
|
|
73
|
-
- Added support for choosing the case ElectroDB will use when modeling a Partition or Sort Key. [[read more]](./README.md#using-electrodb-with-existing-data)
|
|
74
|
-
- Added support for indexes to use fields that are shared with attribute fields. This should help users leverage ElectroDB with existing tables. [[read more]](./README.md#using-electrodb-with-existing-data)
|
|
75
|
-
- Added Query Option `ignoreOwnership` to bypass ElectroDB checks/interrogations for ownership of an item before returning it. [[read more]](./README.md#query-options)
|
|
76
|
-
|
|
77
|
-
## [1.4.1] - 2021-08-25
|
|
78
|
-
### Added
|
|
79
|
-
- Typedef support for RegExp validation on string attributes
|
|
80
|
-
|
|
81
|
-
### Fixed
|
|
82
|
-
- RegExp validation issue resulting in undefined (but not required) values being tested.
|
|
83
|
-
|
|
84
|
-
## [1.4.2] - 2021-09-09
|
|
85
|
-
### Fixed
|
|
86
|
-
- Typing for `.page()` method pager. Now includes the destructured keys associated with the index being queried. [[read more]](./README.md#page)
|
|
87
|
-
- Adding documentation, and expanding typing for the query option `limit`, for use in `.params()` calls. [[read more]](./README.md#query-options)
|
|
88
|
-
|
|
89
|
-
## [1.4.3] - 2021-10-03
|
|
90
|
-
### Fixed
|
|
91
|
-
- ElectroDB would throw when an `undefined` property was passed to query. This has been changed to not throw if a partial query on that index can be accomplished with the data provided.
|
|
92
|
-
|
|
93
|
-
## [1.4.4] - 2021-10-16
|
|
94
|
-
### Added
|
|
95
|
-
- Updates did not include composite attributes involved in primary index. Though these values cannot be changed, they should be `set` on update method calls in case the update results in an item insert. [[read more]](./README.md#updates-to-composite-attributes)
|
|
96
|
-
|
|
97
|
-
## [0.11.1] - 2021-10-17
|
|
98
|
-
### Patched
|
|
99
|
-
- Updates did not include composite attributes involved in primary index. Though these values cannot be changed, they should be `set` on update method calls in case the update results in an item insert. [[read more]](./README.md#updates-to-composite-attributes)
|
|
100
|
-
|
|
101
|
-
## [1.4.5] - 2021-10-17
|
|
102
|
-
### Fixed
|
|
103
|
-
- Improved .npmignore to remove playground oriented files, and created official directory to keep playground in sync with library changes.
|
|
104
|
-
|
|
105
|
-
## [1.4.6] - 2021-10-20
|
|
106
|
-
### Added, Fixed
|
|
107
|
-
- Adding Entity identifiers to all update operations. When primary index composite attributes were added in 1.4.4, entities were written properly but did not include the identifiers. This resulted in entities being written but not being readable without the query option `ignoreOwnership` being used.
|
|
108
|
-
|
|
109
|
-
## [1.4.7] - 2021-10-20
|
|
110
|
-
### Changed
|
|
111
|
-
- Using `add()` update mutation now resolves to `ADD #prop :prop` update expression instead of a `SET #prop = #prop + :prop`
|
|
112
|
-
|
|
113
|
-
### Fixed
|
|
114
|
-
- Fixed param naming conflict during updates, when map attribute shares a name with another (separate) attribute.
|
|
115
|
-
|
|
116
|
-
## [1.4.8] - 2021-11-01
|
|
117
|
-
### Fixed
|
|
118
|
-
- Addressed issue#90 to flip batchGet's response tuple type definition.
|
|
119
|
-
|
|
120
|
-
## [1.5.0] - 2021-11-07
|
|
121
|
-
### Changed
|
|
122
|
-
- Queries will now fully paginate all responses. Prior to this change, ElectroDB would only return items from a single ElectroDB query result. Now ElectroDB will paginate through all query results. This will impact both uses of entity queries and service collections. [[read more](./README.md#query-method)]
|
|
123
|
-
- The query option `limit` has an extended meaning with the change to automatically paginate records on query. The option `limit` now represents a target for the number of items to return from DynamoDB. If this option is passed, Queries on entities and through collections will paginate DynamoDB until this limit is reached or all items for that query have been returned. [[read more](./README.md#query-options)]
|
|
124
|
-
|
|
125
|
-
### Added
|
|
126
|
-
- A new query option `pages` has been added to coincide with the change to automatically paginate all records when queried. The `pages` option sets a max number of pagination iterations ElectroDB will perform on a query. When this option is paired with `limit`, ElectroDB will respect the first condition reached. [[read more](./README.md#query-options)]
|
|
127
|
-
|
|
128
|
-
## [1.6.0] - 2021-11-21
|
|
129
|
-
### Added
|
|
130
|
-
- Exporting TypeScript interfaces for `ElectroError` and `ElectroValidationError`
|
|
131
|
-
- Errors thrown within an attribute's validate callback are now wrapped and accessible after being thrown. Prior to this change, only the `message` of the error thrown by a validation function was persisted back through to the user, now the error itself is also accessible. Reference the exported interface typedef for `ElectroValidationError` [here](./index.d.ts) to see the new properties available on a thrown validation error.
|
|
132
|
-
|
|
133
|
-
### Changed
|
|
134
|
-
- As a byproduct of enhancing validation errors, the format of message text on a validation error has changed. This could be breaking if your app had a hardcoded dependency on the exact text of a thrown validation error.
|
|
135
|
-
|
|
136
|
-
### Fixed
|
|
137
|
-
- For Set attributes, the callback functions `get`, `set`, and `validate` are now consistently given an Array of values. These functions would sometimes (incorrectly) be called with a DynamoDB DocClient Set.
|
|
138
|
-
|
|
139
|
-
## [1.6.1] - 2021-12-05
|
|
140
|
-
### Fixed
|
|
141
|
-
- In some cases the `find()` and `match()` methods would incorrectly select an index without a complete partition key. This would result in validation exceptions preventing the user from querying if an index definition and provided attribute object aligned improperly. This was fixed and a slightly more robust mechanism for ranking indexes was made.
|
|
142
|
-
|
|
143
|
-
## [1.6.2] - 2022-01-27
|
|
144
|
-
### Changed
|
|
145
|
-
- The methods `create`, `patch`, and `remove` will now refer to primary table keys through parameters via ExpressionAttributeNames when using `attribute_exists()`/`attribute_not_exists()` DynamoDB conditions. Prior to this they were referenced directly which would fail in cases where key names include illegal characters. Parameter implementation change only, non-breaking.
|
|
146
|
-
|
|
147
|
-
## [1.6.3] - 2022-02-22
|
|
148
|
-
### Added
|
|
149
|
-
- Add `data` update operation `ifNotExists` to allow for use of the UpdateExpression function "if_not_exists()".
|
|
150
|
-
|
|
151
|
-
## [1.7.0] - 2022-03-13
|
|
152
|
-
### Added
|
|
153
|
-
- New feature: "Listeners". Listeners open the door to some really cool tooling that was not possible because of how ElectroDB augments raw DynamoDB responses and did not provide easy access to raw DyanmoDB parameters. [[read more](./README.md#listeners)]
|
|
154
|
-
|
|
155
|
-
## [1.7.1] - 2022-03-19
|
|
156
|
-
### Added
|
|
157
|
-
- Adding support for the v3 DyanmoDBClient. This change also brings in a new ElectroDB dependency [@aws-sdk/lib-dynamodb](https://www.npmjs.com/package/@aws-sdk/client-dynamodb). [[read more](./README.md#aws-dynamodb-client)]
|
|
158
|
-
|
|
159
|
-
## [1.7.2] - 2022-03-27
|
|
160
|
-
### Fixed
|
|
161
|
-
- Fixed issue#111, `update` method specific query option typing no longer lost when using a `where` method in a query chain
|
|
162
|
-
- Fixing incorrect typing for exposed `UpdateEntityItem` type. Exported type was missing composite key attributes
|
|
163
|
-
|
|
164
|
-
## [1.8.0] - 2022-03-28
|
|
165
|
-
### Added
|
|
166
|
-
- Expected typings for the injected v2 client now include methods for `transactWrite` and `transactGet`
|
|
167
|
-
### Changed
|
|
168
|
-
- Map attributes will now always resolve to least an empty object on a `create` and `put` methods (instead of just the root map)
|
|
169
|
-
- In the past, default values for property attributes on maps only resolves when a user provided an object to place the values on. Now default values within maps attributes will now always resolve onto the object on `create` and `put` methods.
|
|
170
|
-
|
|
171
|
-
## [1.8.1] - 2022-03-29
|
|
172
|
-
### Fixed
|
|
173
|
-
- Solidifying default application methodology: default values for nested properties will be applied up until an undefined default occurs or default callback returns undefined
|