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 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
- > *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.
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
- > 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.
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 | Default | Description
4304
- --------------- | :------------------: | -----------
4305
- 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.
4306
- table | _(from constructor)_ | Use a different table than the one defined in the [Service Options](#service-options)
4307
- raw | `false` | Returns query results as they were returned by the docClient.
4308
- includeKeys | `false` | By default, **ElectroDB** does not return partition, sort, or global keys in its response.
4309
- 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).
4310
- 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.
4311
- 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
4312
- 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).
4313
- 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.
4314
- 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`.
4315
- 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.
4316
- pages | ∞ | How many DynamoDB pages should a query iterate through before stopping. By default ElectroDB paginate through all results for your query.
4317
- listeners | `[]` | An array of callbacks that are invoked when [internal ElectroDB events](#events) occur.
4318
- logger | _none_ | A convenience option for a single event listener that semantically can be used for logging.
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: GoRecord<ResponseType, BulkOptions>;
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>>, Array<AllTableIndexCompositeAttributes<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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
4
4
  "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- let resultsAll = [];
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
- let [results, unprocessed] = this.formatBulkGetResponse(response, config);
337
- for (let r of results) {
338
- resultsAll.push(r);
339
- }
340
- for (let u of unprocessed) {
341
- unprocessedAll.push(u);
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
- formatBulkGetResponse(response = {}, config = {}) {
471
- let unprocessed = [];
472
- let results = [];
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
- unprocessed.push(value);
510
+ unprocessedAll.push(value);
482
511
  } else {
483
- unprocessed.push(
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
- results = this.formatResponse({Items: response.Responses[table]}, index, config);
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 => `"${value}"`).join(", ");
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
  };
@@ -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