electrodb 1.6.3 → 1.7.2

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/CHANGELOG.md CHANGED
@@ -146,4 +146,17 @@ All notable changes to this project will be documented in this file. Breaking ch
146
146
 
147
147
  ## [1.6.3] - 2022-02-22
148
148
  ### Added
149
- - Add `data` update operation `ifNotExists` to allow for use of the UpdateExpression function "if_not_exists()".
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
package/README.md CHANGED
@@ -205,6 +205,14 @@ tasks
205
205
  * [Pagination Example](#pagination-example)
206
206
  * [Query Examples](#query-examples)
207
207
  * [Query Options](#query-options)
208
+ - [AWS DynamoDB Client](#aws-dynamodb-client)
209
+ * [V2 Client](#v2-client)
210
+ * [V3 Client](#v3-client)
211
+ - [Events](#events)
212
+ * [Query Event](#query-event)
213
+ * [Results Event](#results-event)
214
+ - [Logging](#logging)
215
+ - [Listeners](#listeners)
208
216
  - [Errors:](#errors-)
209
217
  + [No Client Defined On Model](#no-client-defined-on-model)
210
218
  + [Invalid Identifier](#invalid-identifier)
@@ -305,9 +313,9 @@ npm install electrodb --save
305
313
  # Usage
306
314
  Require/import `Entity` and/or `Service` from `electrodb`:
307
315
  ```javascript
308
- const {Entity, Service} = require("electrodb");
316
+ const { Entity, Service } = require("electrodb");
309
317
  // or
310
- import {Entity, Service} from "electrodb";
318
+ import { Entity, Service } from "electrodb";
311
319
  ```
312
320
 
313
321
  # Entities and Services
@@ -325,9 +333,9 @@ In ***ElectroDB*** an `Entity` is represents a single business object. For examp
325
333
 
326
334
  Require or import `Entity` from `electrodb`:
327
335
  ```javascript
328
- const {Entity} = require("electrodb");
336
+ const { Entity } = require("electrodb");
329
337
  // or
330
- import {Entity} from "electrodb";
338
+ import { Entity } from "electrodb";
331
339
  ```
332
340
 
333
341
  > When using TypeScript, for strong type checking, be sure to either add your model as an object literal to the Entity constructor or create your model using const assertions with the `as const` syntax.
@@ -337,9 +345,9 @@ In ***ElectroDB*** a `Service` represents a collection of related Entities. Serv
337
345
 
338
346
  Require:
339
347
  ```javascript
340
- const {Service} = require("electrodb");
348
+ const { Service } = require("electrodb");
341
349
  // or
342
- import {Service} from "electrodb";
350
+ import { Service } from "electrodb";
343
351
  ```
344
352
 
345
353
  ## TypeScript Support
@@ -371,7 +379,7 @@ The property name you assign the entity will then be "alias", or name, you can r
371
379
 
372
380
  Services take an optional second parameter, similar to Entities, with a `client` and `table`. Using this constructor interface, the Service will utilize the values from those entities, if they were provided, or be passed values to override the `client` or `table` name on the individual entities.
373
381
 
374
- Not yet available for TypeScript, this pattern will also accept Models, or a mix of Entities and Models, in the same object literal format.
382
+ While not yet typed, this pattern will also accept Models, or a mix of Entities and Models, in the same object literal format.
375
383
 
376
384
  ## Join
377
385
  When using JavaScript, use `join` to add [Entities](#entities) or [Models](#model) onto a Service.
@@ -4000,7 +4008,7 @@ TaskApp.collections
4000
4008
  ```
4001
4009
 
4002
4010
  ## Execute Queries
4003
- Lastly, all query chains end with either a `.go()` or a `.params()` method invocation. These will either execute the query to DynamoDB (`.go()`) or return formatted parameters for use with the DynamoDB docClient (`.params()`).
4011
+ Lastly, all query chains end with either a `.go()`, `.params()`, or `page()` method invocation. These terminal methods will either execute the query to DynamoDB (`.go()`) or return formatted parameters for use with the DynamoDB docClient (`.params()`).
4004
4012
 
4005
4013
  Both `.params()` and `.go()` take a query configuration object which is detailed more in the section [Query Options](#query-options).
4006
4014
 
@@ -4287,6 +4295,8 @@ By default, **ElectroDB** enables you to work with records as the names and prop
4287
4295
  ignoreOwnership?: boolean;
4288
4296
  limit?: number;
4289
4297
  pages?: number;
4298
+ logger?: (event) => void;
4299
+ listeners Array<(event) => void>;
4290
4300
  };
4291
4301
  ```
4292
4302
 
@@ -4304,6 +4314,333 @@ response | `"default"` | Used as a convenience for applying the
4304
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`.
4305
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.
4306
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.
4319
+
4320
+ # AWS DynamoDB Client
4321
+ 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.
4322
+
4323
+ *On the instantiation of an `Entity`:*
4324
+ ```typescript
4325
+ import { Entity } from 'electrodb';
4326
+ import { DocumentClient } from "aws-sdk/clients/dynamodb";
4327
+ const table = "my_table_name";
4328
+ const client = new DocumentClient({
4329
+ region: "us-east-1"
4330
+ });
4331
+
4332
+ const task = new Entity({
4333
+ // your model
4334
+ }, {
4335
+ client, // <----- client
4336
+ table,
4337
+ });
4338
+ ```
4339
+
4340
+ *On the instantiation of an `Service`:*
4341
+ ```typescript
4342
+ import { Entity } from 'electrodb';
4343
+ import { DocumentClient } from "aws-sdk/clients/dynamodb";
4344
+ const table = "my_table_name";
4345
+ const client = new DocumentClient({
4346
+ region: "us-east-1"
4347
+ });
4348
+
4349
+ const task = new Entity({
4350
+ // your model
4351
+ });
4352
+
4353
+ const user = new Entity({
4354
+ // your model
4355
+ });
4356
+
4357
+ const service = new Service({ task, user }, {
4358
+ client, // <----- client
4359
+ table,
4360
+ });
4361
+ ```
4362
+
4363
+ *Via the `setClient` method:*
4364
+ ```typescript
4365
+ import { Entity } from 'electrodb';
4366
+ import { DocumentClient } from "aws-sdk/clients/dynamodb";
4367
+ const table = "my_table_name";
4368
+ const client = new DocumentClient({
4369
+ region: "us-east-1"
4370
+ });
4371
+
4372
+ const task = new Entity({
4373
+ // your model
4374
+ });
4375
+
4376
+ task.setClient(client);
4377
+ ```
4378
+
4379
+ ## V2 Client
4380
+ The [v2](https://www.npmjs.com/package/aws-sdk) sdk will work out of the box with the the DynamoDB DocumentClient.
4381
+
4382
+ *Example:*
4383
+ ```typescript
4384
+ import { DocumentClient } from "aws-sdk/clients/dynamodb";
4385
+ const client = new DocumentClient({
4386
+ region: "us-east-1"
4387
+ });
4388
+ ```
4389
+
4390
+ ## V3 Client
4391
+ The [v3](https://www.npmjs.com/package/@aws-sdk/client-dynamodb) client will work out of the box with the the DynamoDBClient.
4392
+
4393
+ ```typescript
4394
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4395
+ const client = new DynamoDBClient({
4396
+ region: "us-east-1"
4397
+ });
4398
+ ```
4399
+
4400
+ # Logging
4401
+ A logger callback function can be provided both the at the instantiation of an `Entity` or `Service` instance or as a [Query Option](#query-options). The property `logger` is implemented as a convenience property; under the hood ElectroDB uses this property identically to how it uses a [Listener](#listeners).
4402
+
4403
+ *On the instantiation of an `Entity`:*
4404
+ ```typescript
4405
+ import { DynamoDB } from 'aws-sdk';
4406
+ import { Entity, ElectroEvent } from 'electrodb';
4407
+
4408
+ const table = "my_table_name";
4409
+ const client = new DynamoDB.DocumentClient();
4410
+ const logger = (event: ElectroEvent) => {
4411
+ console.log(JSON.stringify(event, null, 4));
4412
+ }
4413
+
4414
+ const task = new Entity({
4415
+ // your model
4416
+ }, {
4417
+ client,
4418
+ table,
4419
+ logger // <----- logger listener
4420
+ });
4421
+ ```
4422
+
4423
+ *On the instantiation of an `Service`:*
4424
+ ```typescript
4425
+ import { DynamoDB } from 'aws-sdk';
4426
+ import { Entity, ElectroEvent } from 'electrodb';
4427
+
4428
+ const table = "my_table_name";
4429
+ const client = new DynamoDB.DocumentClient();
4430
+ const logger = (event: ElectroEvent) => {
4431
+ console.log(JSON.stringify(event, null, 4));
4432
+ }
4433
+
4434
+ const task = new Entity({
4435
+ // your model
4436
+ });
4437
+
4438
+ const user = new Entity({
4439
+ // your model
4440
+ });
4441
+
4442
+ const service = new Service({ task, user }, {
4443
+ client,
4444
+ table,
4445
+ logger // <----- logger listener
4446
+ });
4447
+ ```
4448
+
4449
+ *As a [Query Option](#query-options):*
4450
+ ```typescript
4451
+ const logger = (event: ElectroEvent) => {
4452
+ console.log(JSON.stringify(event, null, 4));
4453
+ }
4454
+
4455
+ task.query
4456
+ .assigned({ userId })
4457
+ .go({ logger });
4458
+ ```
4459
+
4460
+ # Events
4461
+ ElectroDB can be supplied with callbacks (see: [logging](#logging) and [listeners](#listeners) to learn how) to be invoked after certain request lifecycles. This can be useful for logging, analytics, expanding functionality, and more. The following are events currently supported by ElectroDB -- if you would like to see additional events feel free to create a github issue to discuss your concept/need!
4462
+
4463
+ ## Query Event
4464
+ The `query` event occurs when a query is made via the terminal methods [`go()`](#go) and [`page()`](#page). The event includes the exact parameters given to the provided client, the ElectroDB method used, and the ElectroDB configuration provided.
4465
+
4466
+ *Type:*
4467
+ ```typescript
4468
+ interface ElectroQueryEvent<P extends any = any> {
4469
+ type: 'query';
4470
+ method: "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
4471
+ config: any;
4472
+ params: P;
4473
+ }
4474
+ ```
4475
+
4476
+ *Example Input:*
4477
+ ```typescript
4478
+ const prop1 = "22874c81-27c4-4264-92c3-b280aa79aa30";
4479
+ const prop2 = "366aade8-a7c0-4328-8e14-0331b185de4e";
4480
+ const prop3 = "3ec9ed0c-7497-4d05-bdb8-86c09a618047";
4481
+
4482
+ entity.update({ prop1, prop2 })
4483
+ .set({ prop3 })
4484
+ .go()
4485
+ ```
4486
+
4487
+ *Example Output:*
4488
+ ```json
4489
+ {
4490
+ "type": "query",
4491
+ "method": "update",
4492
+ "params": {
4493
+ "UpdateExpression": "SET #prop3 = :prop3_u0, #prop1 = :prop1_u0, #prop2 = :prop2_u0, #__edb_e__ = :__edb_e___u0, #__edb_v__ = :__edb_v___u0",
4494
+ "ExpressionAttributeNames": {
4495
+ "#prop3": "prop3",
4496
+ "#prop1": "prop1",
4497
+ "#prop2": "prop2",
4498
+ "#__edb_e__": "__edb_e__",
4499
+ "#__edb_v__": "__edb_v__"
4500
+ },
4501
+ "ExpressionAttributeValues": {
4502
+ ":prop3_u0": "3ec9ed0c-7497-4d05-bdb8-86c09a618047",
4503
+ ":prop1_u0": "22874c81-27c4-4264-92c3-b280aa79aa30",
4504
+ ":prop2_u0": "366aade8-a7c0-4328-8e14-0331b185de4e",
4505
+ ":__edb_e___u0": "entity",
4506
+ ":__edb_v___u0": "1"
4507
+ },
4508
+ "TableName": "electro",
4509
+ "Key": {
4510
+ "pk": "$test#prop1_22874c81-27c4-4264-92c3-b280aa79aa30",
4511
+ "sk": "$testcollection#entity_1#prop2_366aade8-a7c0-4328-8e14-0331b185de4e"
4512
+ }
4513
+ },
4514
+ "config": { }
4515
+ }
4516
+ ```
4517
+
4518
+ ## Results Event
4519
+ The `results` event occurs when results are returned from DynamoDB. The event includes the exact results returned from the provided client, the ElectroDB method used, and the ElectroDB configuration provided. Note this event handles both failed (or thrown) results in addition to returned (or resolved) results.
4520
+
4521
+ > **Pro-Tip:**
4522
+ > Use this event to hook into the DyanmoDB's [consumed capacity](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-ReturnConsumedCapacity) statistics to learn more about the impact and cost associated with your queries.
4523
+
4524
+ *Type::*
4525
+ ```typescript
4526
+ interface ElectroResultsEvent<R extends any = any> {
4527
+ type: 'results';
4528
+ method: "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
4529
+ config: any;
4530
+ results: R;
4531
+ success: boolean;
4532
+ }
4533
+ ```
4534
+
4535
+ *Example Input:*
4536
+ ```typescript
4537
+ const prop1 = "22874c81-27c4-4264-92c3-b280aa79aa30";
4538
+ const prop2 = "366aade8-a7c0-4328-8e14-0331b185de4e";
4539
+
4540
+ entity.get({ prop1, prop2 }).go();
4541
+ ```
4542
+
4543
+ *Example Output:*
4544
+ ```typescript
4545
+ {
4546
+ "type": "results",
4547
+ "method": "get",
4548
+ "config": { },
4549
+ "success": true,
4550
+ "results": {
4551
+ "Item": {
4552
+ "prop2": "366aade8-a7c0-4328-8e14-0331b185de4e",
4553
+ "sk": "$testcollection#entity_1#prop2_366aade8-a7c0-4328-8e14-0331b185de4e",
4554
+ "prop1": "22874c81-27c4-4264-92c3-b280aa79aa30",
4555
+ "prop3": "3ec9ed0c-7497-4d05-bdb8-86c09a618047",
4556
+ "__edb_e__": "entity",
4557
+ "__edb_v__": "1",
4558
+ "pk": "$test_1#prop1_22874c81-27c4-4264-92c3-b280aa79aa30"
4559
+ }
4560
+ }
4561
+ }
4562
+ ```
4563
+
4564
+ # Listeners
4565
+ ElectroDB can be supplied with callbacks (called "Listeners") to be invoked after certain request lifecycles. Unlike [Attribute Getters and Setters](#attribute-getters-and-setters), Listeners are implemented to react to events passively, not to modify values during the request lifecycle. Listeners can be useful for logging, analytics, expanding functionality, and more. Listeners can be provide both the at the instantiation of an `Entity` or `Service` instance or as a [Query Option](#query-options).
4566
+
4567
+ > _NOTE: Listeners treated as synchronous callbacks and are not awaited. In the event that a callback throws an exception, ElectroDB will quietly catch and log the exception with `console.error` to prevent the exception from impacting your query.
4568
+
4569
+ *On the instantiation of an `Entity`:*
4570
+ ```typescript
4571
+ import { DynamoDB } from 'aws-sdk';
4572
+ import { Entity, ElectroEvent } from 'electrodb';
4573
+
4574
+ const table = "my_table_name";
4575
+ const client = new DynamoDB.DocumentClient();
4576
+ const listener1 = (event: ElectroEvent) => {
4577
+ // do work
4578
+ }
4579
+
4580
+ const listener2 = (event: ElectroEvent) => {
4581
+ // do work
4582
+ }
4583
+
4584
+ const task = new Entity({
4585
+ // your model
4586
+ }, {
4587
+ client,
4588
+ table,
4589
+ listeners: [
4590
+ listener1,
4591
+ listener2, // <----- supports multiple listeners
4592
+ ]
4593
+ });
4594
+ ```
4595
+
4596
+ *On the instantiation of an `Service`:*
4597
+ ```typescript
4598
+ import { DynamoDB } from 'aws-sdk';
4599
+ import { Entity, ElectroEvent } from 'electrodb';
4600
+
4601
+ const table = "my_table_name";
4602
+ const client = new DynamoDB.DocumentClient();
4603
+
4604
+ const listener1 = (event: ElectroEvent) => {
4605
+ // do work
4606
+ }
4607
+
4608
+ const listener2 = (event: ElectroEvent) => {
4609
+ // do work
4610
+ }
4611
+
4612
+ const task = new Entity({
4613
+ // your model
4614
+ });
4615
+
4616
+ const user = new Entity({
4617
+ // your model
4618
+ });
4619
+
4620
+ const service = new Service({ task, user }, {
4621
+ client,
4622
+ table,
4623
+ listeners: [
4624
+ listener1,
4625
+ listener2, // <----- supports multiple listeners
4626
+ ]
4627
+ });
4628
+ ```
4629
+
4630
+ *As a [Query Option](#query-options):*
4631
+ ```typescript
4632
+ const listener1 = (event: ElectroEvent) => {
4633
+ // do work
4634
+ }
4635
+
4636
+ const listener2 = (event: ElectroEvent) => {
4637
+ // do work
4638
+ }
4639
+
4640
+ task.query
4641
+ .assigned({ userId })
4642
+ .go({ listeners: [listener1, listener2] });
4643
+ ```
4307
4644
 
4308
4645
  # Errors:
4309
4646
 
@@ -4311,7 +4648,7 @@ Error Code | Description
4311
4648
  :--------: | --------------------
4312
4649
  1000s | Configuration Errors
4313
4650
  2000s | Invalid Queries
4314
- 3000s | User Defined Errors
4651
+ 3000s | User Defined Errors
4315
4652
  4000s | DynamoDB Errors
4316
4653
  5000s | Unexpected Errors
4317
4654
 
package/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ type Flatten<T> = T extends any[]
2
+ ? T
3
+ : T extends object
4
+ ? {[Key in keyof T]: Flatten<T[Key]>}
5
+ : T;
6
+
1
7
  declare const WhereSymbol: unique symbol;
2
8
  declare const UpdateDataSymbol: unique symbol;
3
9
 
@@ -974,6 +980,8 @@ interface QueryOptions {
974
980
  originalErr?: boolean;
975
981
  ignoreOwnership?: boolean;
976
982
  pages?: number;
983
+ listeners?: Array<EventListener>;
984
+ logger?: EventListener;
977
985
  }
978
986
 
979
987
  // subset of QueryOptions
@@ -985,6 +993,13 @@ interface UpdateQueryOptions extends QueryOptions {
985
993
  response?: "default" | "none" | 'all_old' | 'updated_old' | 'all_new' | 'updated_new';
986
994
  }
987
995
 
996
+ interface UpdateQueryParams {
997
+ response?: "default" | "none" | 'all_old' | 'updated_old' | 'all_new' | 'updated_new';
998
+ table?: string;
999
+ params?: object;
1000
+ originalErr?: boolean;
1001
+ }
1002
+
988
1003
  interface DeleteQueryOptions extends QueryOptions {
989
1004
  response?: "default" | "none" | 'all_old';
990
1005
  }
@@ -1040,7 +1055,13 @@ type SingleRecordOperationOptions<A extends string, F extends A, C extends strin
1040
1055
  type PutRecordOperationOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, ResponseType> = {
1041
1056
  go: GoRecord<ResponseType, PutQueryOptions>;
1042
1057
  params: ParamRecord<PutQueryOptions>;
1043
- where: WhereClause<A,F,C,S,Item<A,F,C,S,S["attributes"]>,SingleRecordOperationOptions<A,F,C,S,ResponseType>>;
1058
+ where: WhereClause<A,F,C,S,Item<A,F,C,S,S["attributes"]>,PutRecordOperationOptions<A,F,C,S,ResponseType>>;
1059
+ };
1060
+
1061
+ type UpdateRecordOperationOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, ResponseType> = {
1062
+ go: GoRecord<ResponseType, UpdateQueryOptions>;
1063
+ params: ParamRecord<UpdateQueryParams>;
1064
+ where: WhereClause<A,F,C,S,Item<A,F,C,S,S["attributes"]>,PutRecordOperationOptions<A,F,C,S,ResponseType>>;
1044
1065
  };
1045
1066
 
1046
1067
  type DeleteRecordOperationOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, ResponseType> = {
@@ -1056,7 +1077,7 @@ type BulkRecordOperationOptions<A extends string, F extends A, C extends string,
1056
1077
 
1057
1078
  type SetRecordActionOptions<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, SetAttr,IndexCompositeAttributes,TableItem> = {
1058
1079
  go: GoRecord<Partial<TableItem>, UpdateQueryOptions>;
1059
- params: ParamRecord<UpdateQueryOptions>;
1080
+ params: ParamRecord<UpdateQueryParams>;
1060
1081
  set: SetRecord<A,F,C,S, SetItem<A,F,C,S>,IndexCompositeAttributes,TableItem>;
1061
1082
  remove: SetRecord<A,F,C,S, Array<keyof SetItem<A,F,C,S>>,IndexCompositeAttributes,TableItem>;
1062
1083
  add: SetRecord<A,F,C,S, AddItem<A,F,C,S>,IndexCompositeAttributes,TableItem>;
@@ -1064,7 +1085,7 @@ type SetRecordActionOptions<A extends string, F extends A, C extends string, S e
1064
1085
  append: SetRecord<A,F,C,S, AppendItem<A,F,C,S>,IndexCompositeAttributes,TableItem>;
1065
1086
  delete: SetRecord<A,F,C,S, DeleteItem<A,F,C,S>,IndexCompositeAttributes,TableItem>;
1066
1087
  data: DataUpdateMethodRecord<A,F,C,S, Item<A,F,C,S,S["attributes"]>,IndexCompositeAttributes,TableItem>;
1067
- where: WhereClause<A,F,C,S, Item<A,F,C,S,S["attributes"]>,RecordsActionOptions<A,F,C,S,TableItem,IndexCompositeAttributes>>;
1088
+ where: WhereClause<A,F,C,S, Item<A,F,C,S,S["attributes"]>,SetRecordActionOptions<A,F,C,S,SetAttr,IndexCompositeAttributes,TableItem>>;
1068
1089
  }
1069
1090
 
1070
1091
  type SetRecord<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, SetAttr, IndexCompositeAttributes, TableItem> = (properties: SetAttr) => SetRecordActionOptions<A,F,C,S, SetAttr, IndexCompositeAttributes, TableItem>;
@@ -1118,23 +1139,77 @@ type DocumentClient = {
1118
1139
  batchWrite: DocumentClientMethod;
1119
1140
  batchGet: DocumentClientMethod;
1120
1141
  scan: DocumentClientMethod;
1121
- }
1142
+ } | {
1143
+ send: (command: any) => Promise<any>;
1144
+ }
1145
+
1146
+ type ElectroDBMethodTypes = "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
1147
+
1148
+ interface ElectroQueryEvent<P extends any = any> {
1149
+ type: 'query';
1150
+ method: ElectroDBMethodTypes;
1151
+ config: any;
1152
+ params: P;
1153
+ }
1154
+
1155
+ interface ElectroResultsEvent<R extends any = any> {
1156
+ type: 'results';
1157
+ method: ElectroDBMethodTypes;
1158
+ config: any;
1159
+ results: R;
1160
+ success: boolean;
1161
+ }
1162
+
1163
+ type ElectroEvent =
1164
+ ElectroQueryEvent
1165
+ | ElectroResultsEvent;
1166
+
1167
+ type ElectroEventType = Pick<ElectroEvent, 'type'>;
1168
+
1169
+ type EventListener = (event: ElectroEvent) => void;
1170
+
1171
+ // todo: coming soon, more events!
1172
+ // | {
1173
+ // name: "error";
1174
+ // type: "configuration_error" | "invalid_query" | "dynamodb_client";
1175
+ // message: string;
1176
+ // details: ElectroError;
1177
+ // } | {
1178
+ // name: "error";
1179
+ // type: "user_defined";
1180
+ // message: string;
1181
+ // details: ElectroValidationError;
1182
+ // } | {
1183
+ // name: "warn";
1184
+ // type: "deprecation_warning" | "optimization_suggestion";
1185
+ // message: string;
1186
+ // details: any;
1187
+ // } | {
1188
+ // name: "info";
1189
+ // type: "client_updated" | "table_overwritten";
1190
+ // message: string;
1191
+ // details: any;
1192
+ // };
1122
1193
 
1123
1194
  type EntityConfiguration = {
1124
1195
  table?: string;
1125
- client?: DocumentClient
1196
+ client?: DocumentClient;
1197
+ listeners?: Array<EventListener>;
1198
+ logger?: EventListener;
1126
1199
  };
1127
1200
 
1128
1201
  type ServiceConfiguration = {
1129
1202
  table?: string;
1130
- client?: DocumentClient
1203
+ client?: DocumentClient;
1204
+ listeners?: Array<EventListener>;
1205
+ logger?: EventListener;
1131
1206
  };
1132
1207
 
1133
1208
  type ParseSingleInput = {
1134
- Item?: {[key: string]: any}
1135
- } | {
1136
- Attributes?: {[key: string]: any}
1137
- } | null
1209
+ Item?: {[key: string]: any}
1210
+ } | {
1211
+ Attributes?: {[key: string]: any}
1212
+ } | null;
1138
1213
 
1139
1214
  type ParseMultiInput = {
1140
1215
  Items?: {[key: string]: any}[]
@@ -1426,7 +1501,7 @@ export type CreateEntityItem<E extends Entity<any, any, any, any>> =
1426
1501
 
1427
1502
  export type UpdateEntityItem<E extends Entity<any, any, any, any>> =
1428
1503
  E extends Entity<infer A, infer F, infer C, infer S>
1429
- ? SetItem<A, F, C, S>
1504
+ ? Partial<ResponseItem<A,F,C,S>>
1430
1505
  : never;
1431
1506
 
1432
1507
  export type UpdateAddEntityItem<E extends Entity<any, any, any, any>> =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "1.6.3",
3
+ "version": "1.7.2",
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": {
@@ -25,6 +25,8 @@
25
25
  },
26
26
  "homepage": "https://github.com/tywalch/electrodb#readme",
27
27
  "devDependencies": {
28
+ "@aws-sdk/client-dynamodb": "^3.54.1",
29
+ "@aws-sdk/lib-dynamodb": "^3.54.1",
28
30
  "@istanbuljs/nyc-config-typescript": "^1.0.2",
29
31
  "@types/chai": "^4.2.12",
30
32
  "@types/mocha": "^8.0.3",
@@ -59,6 +61,7 @@
59
61
  "directory": "test"
60
62
  },
61
63
  "dependencies": {
62
- "jsonschema": "1.2.7"
64
+ "jsonschema": "1.2.7",
65
+ "@aws-sdk/lib-dynamodb": "^3.54.1"
63
66
  }
64
67
  }
package/src/clauses.js CHANGED
@@ -540,8 +540,9 @@ let clauses = {
540
540
  if (!v.isStringHasLength(options.table) && !v.isStringHasLength(entity._getTableName())) {
541
541
  throw new e.ElectroError(e.ErrorCodes.MissingTable, `Table name not defined. Table names must be either defined on the model, instance configuration, or as a query option.`);
542
542
  }
543
+ const method = state.getMethod();
543
544
  let results;
544
- switch (state.getMethod()) {
545
+ switch (method) {
545
546
  case MethodTypes.query:
546
547
  results = entity._queryParams(state, options);
547
548
  break;
@@ -556,7 +557,7 @@ let clauses = {
556
557
  break;
557
558
  }
558
559
 
559
- if (state.getMethod() === MethodTypes.update && results.ExpressionAttributeValues && Object.keys(results.ExpressionAttributeValues).length === 0) {
560
+ if (method === MethodTypes.update && results.ExpressionAttributeValues && Object.keys(results.ExpressionAttributeValues).length === 0) {
560
561
  // An update that only does a `remove` operation would result in an empty object
561
562
  // todo: change the getValues() method to return undefined in this case (would potentially require a more generous refactor)
562
563
  delete results.ExpressionAttributeValues;
package/src/client.js ADDED
@@ -0,0 +1,135 @@
1
+ const {isFunction} = require('./validations');
2
+ const {ElectroError, ErrorCodes} = require('./errors');
3
+ const lib = require('@aws-sdk/lib-dynamodb');
4
+
5
+ const DocumentClientVersions = {
6
+ v2: 'v2',
7
+ v3: 'v3',
8
+ electro: 'electro',
9
+ };
10
+
11
+ const v3Methods = ['send'];
12
+ const v2Methods = ['get', 'put', 'update', 'delete', 'batchWrite', 'batchGet', 'scan', 'query', 'createSet'];
13
+ const supportedClientVersions = {
14
+ [DocumentClientVersions.v2]: v2Methods,
15
+ [DocumentClientVersions.v3]: v3Methods,
16
+ }
17
+
18
+ class DocumentClientV3Wrapper {
19
+ static init(client) {
20
+ return new DocumentClientV3Wrapper(client, lib);
21
+ }
22
+
23
+ constructor(client, lib) {
24
+ this.client = client;
25
+ this.lib = lib;
26
+ }
27
+
28
+ promiseWrap(fn) {
29
+ return {
30
+ promise: async () => {
31
+ return fn();
32
+ }
33
+ }
34
+ }
35
+
36
+ get(params) {
37
+ return this.promiseWrap(() => {
38
+ const command = new this.lib.GetCommand(params);
39
+ return this.client.send(command);
40
+ });
41
+ }
42
+ put(params) {
43
+ return this.promiseWrap(() => {
44
+ const command = new this.lib.PutCommand(params);
45
+ return this.client.send(command);
46
+ });
47
+ }
48
+ update(params) {
49
+ return this.promiseWrap(() => {
50
+ const command = new this.lib.UpdateCommand(params);
51
+ return this.client.send(command);
52
+ });
53
+ }
54
+ delete(params) {
55
+ return this.promiseWrap(async () => {
56
+ const command = new this.lib.DeleteCommand(params);
57
+ return this.client.send(command);
58
+ });
59
+ }
60
+ batchWrite(params) {
61
+ return this.promiseWrap(async () => {
62
+ const command = new this.lib.BatchWriteCommand(params);
63
+ return this.client.send(command);
64
+ });
65
+ }
66
+ batchGet(params) {
67
+ return this.promiseWrap(async () => {
68
+ const command = new this.lib.BatchGetCommand(params);
69
+ return this.client.send(command);
70
+ });
71
+ }
72
+ scan(params) {
73
+ return this.promiseWrap(async () => {
74
+ const command = new this.lib.ScanCommand(params);
75
+ return this.client.send(command);
76
+ });
77
+ }
78
+ query(params) {
79
+ return this.promiseWrap(async () => {
80
+ const command = new this.lib.QueryCommand(params);
81
+ return this.client.send(command);
82
+ });
83
+ }
84
+ createSet(value) {
85
+ if (Array.isArray(value)) {
86
+ return new Set(value);
87
+ } else {
88
+ return new Set([value]);
89
+ }
90
+ }
91
+ }
92
+
93
+ function identifyClientVersion(client = {}) {
94
+ if (client instanceof DocumentClientV3Wrapper) return DocumentClientVersions.electro;
95
+ for (const [version, methods] of Object.entries(supportedClientVersions)) {
96
+ const hasMethods = methods.every(method => {
97
+ return method in client && isFunction(client[method]);
98
+ });
99
+ if (hasMethods) {
100
+ return version;
101
+ }
102
+ }
103
+ }
104
+
105
+ function normalizeClient(client) {
106
+ if (client === undefined) return client;
107
+ const version = identifyClientVersion(client);
108
+ switch(version) {
109
+ case DocumentClientVersions.v3:
110
+ return DocumentClientV3Wrapper.init(client);
111
+ case DocumentClientVersions.v2:
112
+ case DocumentClientVersions.electro:
113
+ return client;
114
+ default:
115
+ throw new ElectroError(ErrorCodes.InvalidClientProvided, 'Invalid DynamoDB Document Client provided. ElectroDB supports the v2 and v3 DynamoDB Document Clients from the aws-sdk');
116
+ }
117
+ }
118
+
119
+ function normalizeConfig(config = {}) {
120
+ return {
121
+ ...config,
122
+ client: normalizeClient(config.client)
123
+ }
124
+ }
125
+
126
+ module.exports = {
127
+ v2Methods,
128
+ v3Methods,
129
+ normalizeClient,
130
+ normalizeConfig,
131
+ identifyClientVersion,
132
+ DocumentClientVersions,
133
+ supportedClientVersions,
134
+ DocumentClientV3Wrapper,
135
+ };
package/src/entity.js CHANGED
@@ -5,12 +5,19 @@ const { FilterFactory } = require("./filters");
5
5
  const { FilterOperations } = require("./operations");
6
6
  const { WhereFactory } = require("./where");
7
7
  const { clauses, ChainState } = require("./clauses");
8
+ const {EventManager} = require('./events');
8
9
  const validations = require("./validations");
9
- const utilities = require("./util");
10
+ const c = require('./client');
11
+ const u = require("./util");
10
12
  const e = require("./errors");
11
13
 
12
14
  class Entity {
13
15
  constructor(model, config = {}) {
16
+ config = c.normalizeConfig(config);
17
+ this.eventManager = new EventManager({
18
+ listeners: config.listeners
19
+ });
20
+ this.eventManager.add(config.logger);
14
21
  this._validateModel(model);
15
22
  this.version = EntityVersions.v1;
16
23
  this.config = config;
@@ -43,7 +50,7 @@ class Entity {
43
50
 
44
51
  setIdentifier(type = "", identifier = "") {
45
52
  if (!this.identifiers[type]) {
46
- throw new e.ElectroError(e.ErrorCodes.InvalidIdentifier, `Invalid identifier type: "${type}". Valid identifiers include: ${utilities.commaSeparatedString(Object.keys(this.identifiers))}`);
53
+ throw new e.ElectroError(e.ErrorCodes.InvalidIdentifier, `Invalid identifier type: "${type}". Valid identifiers include: ${u.commaSeparatedString(Object.keys(this.identifiers))}`);
47
54
  } else {
48
55
  this.identifiers[type] = identifier;
49
56
  }
@@ -252,9 +259,32 @@ class Entity {
252
259
  }
253
260
  }
254
261
 
255
- async _exec(method, parameters) {
256
- return this.client[method](parameters).promise()
262
+ async _exec(method, params, config = {}) {
263
+ const notifyQuery = () => {
264
+ this.eventManager.trigger({
265
+ type: "query",
266
+ method,
267
+ params,
268
+ config,
269
+ }, config.listeners);
270
+ };
271
+ const notifyResults = (results, success) => {
272
+ this.eventManager.trigger({
273
+ type: "results",
274
+ method,
275
+ config,
276
+ success,
277
+ results,
278
+ }, config.listeners);
279
+ }
280
+ return this.client[method](params).promise()
281
+ .then((results) => {
282
+ notifyQuery();
283
+ notifyResults(results, true);
284
+ return results;
285
+ })
257
286
  .catch(err => {
287
+ notifyResults(err, false);
258
288
  err.__isAWSError = true;
259
289
  throw err;
260
290
  });
@@ -266,10 +296,10 @@ class Entity {
266
296
  }
267
297
  let results = [];
268
298
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
269
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
299
+ let concurrentOperations = u.batchItems(parameters, concurrent);
270
300
  for (let operation of concurrentOperations) {
271
301
  await Promise.all(operation.map(async params => {
272
- let response = await this._exec(MethodTypes.batchWrite, params);
302
+ let response = await this._exec(MethodTypes.batchWrite, params, config);
273
303
  if (validations.isFunction(config.parse)) {
274
304
  let parsed = await config.parse(config, response);
275
305
  if (parsed) {
@@ -292,13 +322,13 @@ class Entity {
292
322
  parameters = [parameters];
293
323
  }
294
324
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
295
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
325
+ let concurrentOperations = u.batchItems(parameters, concurrent);
296
326
 
297
327
  let resultsAll = [];
298
328
  let unprocessedAll = [];
299
329
  for (let operation of concurrentOperations) {
300
330
  await Promise.all(operation.map(async params => {
301
- let response = await this._exec(MethodTypes.batchGet, params);
331
+ let response = await this._exec(MethodTypes.batchGet, params, config);
302
332
  if (validations.isFunction(config.parse)) {
303
333
  resultsAll.push(await config.parse(config, response));
304
334
  return;
@@ -328,7 +358,7 @@ class Entity {
328
358
  let limit = max === undefined
329
359
  ? parameters.Limit
330
360
  : max - count;
331
- let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit});
361
+ let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit}, config);
332
362
 
333
363
  ExclusiveStartKey = response.LastEvaluatedKey;
334
364
 
@@ -363,7 +393,7 @@ class Entity {
363
393
  }
364
394
 
365
395
  async executeOperation(method, parameters, config) {
366
- let response = await this._exec(method, parameters);
396
+ let response = await this._exec(method, parameters, config);
367
397
  if (validations.isFunction(config.parse)) {
368
398
  return config.parse(config, response);
369
399
  }
@@ -560,7 +590,7 @@ class Entity {
560
590
 
561
591
  _setClient(client) {
562
592
  if (client) {
563
- this.client = client;
593
+ this.client = c.normalizeClient(client);
564
594
  }
565
595
  }
566
596
 
@@ -751,13 +781,14 @@ class Entity {
751
781
  _isPagination: false,
752
782
  _isCollectionQuery: false,
753
783
  pages: undefined,
784
+ listeners: [],
754
785
  };
755
786
 
756
787
  config = options.reduce((config, option) => {
757
788
  if (typeof option.response === 'string' && option.response.length) {
758
789
  const format = ReturnValues[option.response];
759
790
  if (format === undefined) {
760
- throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for query option "format" provided: "${option.format}". Allowed values include ${utilities.commaSeparatedString(Object.keys(ReturnValues))}.`);
791
+ throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for query option "format" provided: "${option.format}". Allowed values include ${u.commaSeparatedString(Object.keys(ReturnValues))}.`);
761
792
  }
762
793
  config.response = format;
763
794
  config.params.ReturnValues = FormatToReturnValues[format];
@@ -814,7 +845,7 @@ class Entity {
814
845
  if (typeof Pager[option.pager] === "string") {
815
846
  config.pager = option.pager;
816
847
  } else {
817
- throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "pager" provided: "${option.pager}". Allowed values include ${utilities.commaSeparatedString(Object.keys(Pager))}.`);
848
+ throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "pager" provided: "${option.pager}". Allowed values include ${u.commaSeparatedString(Object.keys(Pager))}.`);
818
849
  }
819
850
  }
820
851
 
@@ -822,7 +853,7 @@ class Entity {
822
853
  if (typeof UnprocessedTypes[option.unprocessed] === "string") {
823
854
  config.unproessed = UnprocessedTypes[option.unprocessed];
824
855
  } else {
825
- throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "unprocessed" provided: "${option.unprocessed}". Allowed values include ${utilities.commaSeparatedString(Object.keys(UnprocessedTypes))}.`);
856
+ throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "unprocessed" provided: "${option.unprocessed}". Allowed values include ${u.commaSeparatedString(Object.keys(UnprocessedTypes))}.`);
826
857
  }
827
858
  }
828
859
 
@@ -830,6 +861,20 @@ class Entity {
830
861
  config.ignoreOwnership = option.ignoreOwnership;
831
862
  }
832
863
 
864
+ if (option.listeners) {
865
+ if (Array.isArray(option.listeners)) {
866
+ config.listeners = config.listeners.concat(option.listeners);
867
+ }
868
+ }
869
+
870
+ if (option.logger) {
871
+ if (validations.isFunction(option.logger)) {
872
+ config.listeners.push(option.logger);
873
+ } else {
874
+ throw new e.ElectroError(e.ErrorCodes.InvalidLoggerProvided, `Loggers must be of type function`);
875
+ }
876
+ }
877
+
833
878
  config.page = Object.assign({}, config.page, option.page);
834
879
  config.params = Object.assign({}, config.params, option.params);
835
880
  return config;
@@ -933,7 +978,7 @@ class Entity {
933
978
  records.push(Key);
934
979
  }
935
980
  }
936
- let batches = utilities.batchItems(records, MaxBatchItems.batchGet);
981
+ let batches = u.batchItems(records, MaxBatchItems.batchGet);
937
982
  return batches.map(batch => {
938
983
  return {
939
984
  RequestItems: {
@@ -966,7 +1011,7 @@ class Entity {
966
1011
  throw new Error("Invalid method type");
967
1012
  }
968
1013
  }
969
- let batches = utilities.batchItems(records, MaxBatchItems.batchWrite);
1014
+ let batches = u.batchItems(records, MaxBatchItems.batchWrite);
970
1015
  return batches.map(batch => {
971
1016
  return {
972
1017
  RequestItems: {
@@ -1220,7 +1265,7 @@ class Entity {
1220
1265
  let props = Object.keys(item);
1221
1266
  let missing = require.filter((prop) => !props.includes(prop));
1222
1267
  if (!missing) {
1223
- throw new e.ElectroError(e.ErrorCodes.MissingAttribute, `Item is missing attributes: ${utilities.commaSeparatedString(missing)}`);
1268
+ throw new e.ElectroError(e.ErrorCodes.MissingAttribute, `Item is missing attributes: ${u.commaSeparatedString(missing)}`);
1224
1269
  }
1225
1270
  }
1226
1271
 
@@ -1229,7 +1274,7 @@ class Entity {
1229
1274
  throw new Error(`Invalid attribute ${prop}`);
1230
1275
  }
1231
1276
  if (restrict.length && !restrict.includes(prop)) {
1232
- throw new Error(`${prop} is not a valid attribute: ${utilities.commaSeparatedString(restrict)}`);
1277
+ throw new Error(`${prop} is not a valid attribute: ${u.commaSeparatedString(restrict)}`);
1233
1278
  }
1234
1279
  if (prop === undefined || skip.includes(prop)) {
1235
1280
  continue;
@@ -1392,7 +1437,7 @@ class Entity {
1392
1437
  _makeComparisonQueryParams(index = TableIndex, comparison = "", filter = {}, pk = {}, sk = {}) {
1393
1438
  let operator = Comparisons[comparison];
1394
1439
  if (!operator) {
1395
- throw new Error(`Unexpected comparison operator "${comparison}", expected ${utilities.commaSeparatedString(Object.values(Comparisons))}`);
1440
+ throw new Error(`Unexpected comparison operator "${comparison}", expected ${u.commaSeparatedString(Object.values(Comparisons))}`);
1396
1441
  }
1397
1442
  let keyExpressions = this._queryKeyExpressionAttributeBuilder(
1398
1443
  index,
@@ -1429,7 +1474,7 @@ class Entity {
1429
1474
  let incompleteAccessPatterns = incomplete.map(({index}) => this.model.translations.indexes.fromIndexToAccessPattern[index]);
1430
1475
  let missingFacets = incomplete.reduce((result, { missing }) => [...result, ...missing], []);
1431
1476
  throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes,
1432
- `Incomplete composite attributes: Without the composite attributes ${utilities.commaSeparatedString(missingFacets)} the following access patterns cannot be updated: ${utilities.commaSeparatedString(incompleteAccessPatterns.filter((val) => val !== undefined))} `,
1477
+ `Incomplete composite attributes: Without the composite attributes ${u.commaSeparatedString(missingFacets)} the following access patterns cannot be updated: ${u.commaSeparatedString(incompleteAccessPatterns.filter((val) => val !== undefined))} `,
1433
1478
  );
1434
1479
  }
1435
1480
  return complete;
@@ -1648,7 +1693,7 @@ class Entity {
1648
1693
  _expectFacets(obj = {}, properties = [], type = "key composite attributes") {
1649
1694
  let [incompletePk, missing, matching] = this._expectProperties(obj, properties);
1650
1695
  if (incompletePk) {
1651
- throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes, `Incomplete or invalid ${type} supplied. Missing properties: ${utilities.commaSeparatedString(missing)}`);
1696
+ throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes, `Incomplete or invalid ${type} supplied. Missing properties: ${u.commaSeparatedString(missing)}`);
1652
1697
  } else {
1653
1698
  return matching;
1654
1699
  }
@@ -1721,10 +1766,10 @@ class Entity {
1721
1766
 
1722
1767
  // If keys arent custom, set the prefixes
1723
1768
  if (!keys.pk.isCustom) {
1724
- keys.pk.prefix = utilities.formatKeyCasing(pk, tableIndex.pk.casing);
1769
+ keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
1725
1770
  }
1726
1771
  if (!keys.sk.isCustom) {
1727
- keys.sk.prefix = utilities.formatKeyCasing(sk, tableIndex.sk.casing);
1772
+ keys.sk.prefix = u.formatKeyCasing(sk, tableIndex.sk.casing);
1728
1773
  }
1729
1774
 
1730
1775
  return keys;
@@ -1735,7 +1780,7 @@ class Entity {
1735
1780
  ? this.model.indexes[accessPattern].sk.casing
1736
1781
  : undefined;
1737
1782
 
1738
- return utilities.formatKeyCasing(key, casing);
1783
+ return u.formatKeyCasing(key, casing);
1739
1784
  }
1740
1785
 
1741
1786
  _validateIndex(index) {
@@ -1859,7 +1904,7 @@ class Entity {
1859
1904
  key = `${key}${supplied[name]}`;
1860
1905
  }
1861
1906
 
1862
- return utilities.formatKeyCasing(key, casing);
1907
+ return u.formatKeyCasing(key, casing);
1863
1908
  }
1864
1909
 
1865
1910
  _findBestIndexKeyMatch(attributes = {}) {
@@ -2139,7 +2184,7 @@ class Entity {
2139
2184
  let indexName = index.index || TableIndex;
2140
2185
  if (seenIndexes[indexName] !== undefined) {
2141
2186
  if (indexName === TableIndex) {
2142
- throw new e.ElectroError(e.ErrorCodes.DuplicateIndexes, `Duplicate index defined in model found in Access Pattern '${accessPattern}': '${utilities.formatIndexNameForDisplay(indexName)}'. This could be because you forgot to specify the index name of a secondary index defined in your model.`);
2187
+ throw new e.ElectroError(e.ErrorCodes.DuplicateIndexes, `Duplicate index defined in model found in Access Pattern '${accessPattern}': '${u.formatIndexNameForDisplay(indexName)}'. This could be because you forgot to specify the index name of a secondary index defined in your model.`);
2143
2188
  } else {
2144
2189
  throw new e.ElectroError(e.ErrorCodes.DuplicateIndexes, `Duplicate index defined in model found in Access Pattern '${accessPattern}': '${indexName}'`);
2145
2190
  }
@@ -2148,7 +2193,7 @@ class Entity {
2148
2193
  let hasSk = !!index.sk;
2149
2194
  let inCollection = !!index.collection;
2150
2195
  if (!hasSk && inCollection) {
2151
- throw new e.ElectroError(e.ErrorCodes.CollectionNoSK, `Invalid Access pattern definition for '${accessPattern}': '${utilities.formatIndexNameForDisplay(indexName)}', contains a collection definition without a defined SK. Collections can only be defined on indexes with a defined SK.`);
2196
+ throw new e.ElectroError(e.ErrorCodes.CollectionNoSK, `Invalid Access pattern definition for '${accessPattern}': '${u.formatIndexNameForDisplay(indexName)}', contains a collection definition without a defined SK. Collections can only be defined on indexes with a defined SK.`);
2152
2197
  }
2153
2198
  let collection = index.collection || "";
2154
2199
  let customFacets = {
@@ -2219,7 +2264,7 @@ class Entity {
2219
2264
  if (Array.isArray(sk.facets)) {
2220
2265
  let duplicates = pk.facets.filter(facet => sk.facets.includes(facet));
2221
2266
  if (duplicates.length !== 0) {
2222
- throw new e.ElectroError(e.ErrorCodes.DuplicateIndexCompositeAttributes, `The Access Pattern '${accessPattern}' contains duplicate references the composite attribute(s): ${utilities.commaSeparatedString(duplicates)}. Composite attributes may only be used once within an index. If this leaves the Sort Key (sk) without any composite attributes simply set this to be an empty array.`);
2267
+ throw new e.ElectroError(e.ErrorCodes.DuplicateIndexCompositeAttributes, `The Access Pattern '${accessPattern}' contains duplicate references the composite attribute(s): ${u.commaSeparatedString(duplicates)}. Composite attributes may only be used once within an index. If this leaves the Sort Key (sk) without any composite attributes simply set this to be an empty array.`);
2223
2268
  }
2224
2269
  }
2225
2270
 
@@ -2322,13 +2367,13 @@ class Entity {
2322
2367
 
2323
2368
  let pkTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedPKAttributes, index.pk.composite);
2324
2369
  if (!pkTemplateIsCompatible) {
2325
- throw new e.ElectroError(e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate, `Incompatible PK 'template' and 'composite' properties for defined on index "${utilities.formatIndexNameForDisplay(indexName)}". PK "template" string is defined as having composite attributes ${utilities.commaSeparatedString(parsedPKAttributes.attributes)} while PK "composite" array is defined with composite attributes ${utilities.commaSeparatedString(index.pk.composite)}`);
2370
+ throw new e.ElectroError(e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate, `Incompatible PK 'template' and 'composite' properties for defined on index "${u.formatIndexNameForDisplay(indexName)}". PK "template" string is defined as having composite attributes ${u.commaSeparatedString(parsedPKAttributes.attributes)} while PK "composite" array is defined with composite attributes ${u.commaSeparatedString(index.pk.composite)}`);
2326
2371
  }
2327
2372
 
2328
2373
  if (index.sk !== undefined && Array.isArray(index.sk.composite) && typeof index.sk.template === "string") {
2329
2374
  let skTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedSKAttributes, index.sk.composite);
2330
2375
  if (!skTemplateIsCompatible) {
2331
- throw new e.ElectroError(e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate, `Incompatible SK 'template' and 'composite' properties for defined on index "${utilities.formatIndexNameForDisplay(indexName)}". SK "template" string is defined as having composite attributes ${utilities.commaSeparatedString(parsedSKAttributes.attributes)} while SK "composite" array is defined with composite attributes ${utilities.commaSeparatedString(index.sk.composite)}`);
2376
+ throw new e.ElectroError(e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate, `Incompatible SK 'template' and 'composite' properties for defined on index "${u.formatIndexNameForDisplay(indexName)}". SK "template" string is defined as having composite attributes ${u.commaSeparatedString(parsedSKAttributes.attributes)} while SK "composite" array is defined with composite attributes ${u.commaSeparatedString(index.sk.composite)}`);
2332
2377
  }
2333
2378
  }
2334
2379
  }
@@ -2356,7 +2401,7 @@ class Entity {
2356
2401
 
2357
2402
  for (let [name, fn] of Object.entries(filters)) {
2358
2403
  if (invalidFilterNames.includes(name)) {
2359
- throw new e.ElectroError(e.ErrorCodes.InvalidFilter, `Invalid filter name: ${name}. Filter cannot be named ${utilities.commaSeparatedString(invalidFilterNames)}`);
2404
+ throw new e.ElectroError(e.ErrorCodes.InvalidFilter, `Invalid filter name: ${name}. Filter cannot be named ${u.commaSeparatedString(invalidFilterNames)}`);
2360
2405
  } else {
2361
2406
  normalized[name] = fn;
2362
2407
  }
@@ -2475,7 +2520,7 @@ class Entity {
2475
2520
  _parseModel(model, config = {}) {
2476
2521
  /** start beta/v1 condition **/
2477
2522
  const {client} = config;
2478
- let modelVersion = utilities.getModelVersion(model);
2523
+ let modelVersion = u.getModelVersion(model);
2479
2524
  let service, entity, version, table, name;
2480
2525
  switch(modelVersion) {
2481
2526
  case ModelVersions.beta:
package/src/errors.js CHANGED
@@ -127,6 +127,18 @@ const ErrorCodes = {
127
127
  name: "InvalidIndexCompositeWithAttributeName",
128
128
  sym: ErrorCode,
129
129
  },
130
+ InvalidListenerProvided: {
131
+ code: 1020,
132
+ section: "invalid-listener-provided",
133
+ name: "InvalidListenerProvided",
134
+ sym: ErrorCode,
135
+ },
136
+ InvalidClientProvided: {
137
+ code: 1021,
138
+ section: "invalid-client-provided",
139
+ name: "InvalidClientProvided",
140
+ sym: ErrorCode,
141
+ },
130
142
  MissingAttribute: {
131
143
  code: 2001,
132
144
  section: "missing-attribute",
@@ -204,7 +216,7 @@ const ErrorCodes = {
204
216
  section: "pager-not-unique",
205
217
  name: "NoOwnerForPager",
206
218
  sym: ErrorCode,
207
- },
219
+ }
208
220
  };
209
221
 
210
222
  function makeMessage(message, section) {
package/src/events.js ADDED
@@ -0,0 +1,67 @@
1
+ const e = require("./errors");
2
+ const v = require('./validations');
3
+
4
+ class EventManager {
5
+ static createSafeListener(listener) {
6
+ if (listener === undefined) {
7
+ return undefined;
8
+ } if (!v.isFunction(listener)) {
9
+ throw new e.ElectroError(e.ErrorCodes.InvalidListenerProvided, `Provided listener is not of type 'function'`);
10
+ } else {
11
+ return (...params) => {
12
+ try {
13
+ listener(...params);
14
+ } catch(err) {
15
+ console.error(`Error invoking user supplied listener`, err);
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ static normalizeListeners(listeners = []) {
22
+ if (!Array.isArray(listeners)) {
23
+ throw new e.ElectroError(e.ErrorCodes.InvalidListenerProvided, `Listeners must be provided as an array of functions`);
24
+ }
25
+ return listeners
26
+ .map(listener => EventManager.createSafeListener(listener))
27
+ .filter(listener => {
28
+ switch (typeof listener) {
29
+ case 'function':
30
+ return true;
31
+ case 'undefined':
32
+ return false;
33
+ default:
34
+ throw new e.ElectroError(e.ErrorCodes.InvalidListenerProvided, `Provided listener is not of type 'function`);
35
+ }
36
+ });
37
+ }
38
+
39
+ constructor({listeners = []} = {}) {
40
+ this.listeners = EventManager.normalizeListeners(listeners);
41
+ }
42
+
43
+ add(listeners = []) {
44
+ if (!Array.isArray(listeners)) {
45
+ listeners = [listeners];
46
+ }
47
+
48
+ this.listeners = this.listeners.concat(
49
+ EventManager.normalizeListeners(listeners)
50
+ );
51
+ }
52
+
53
+ trigger(event, adHocListeners = []) {
54
+ const allListeners = [
55
+ ...this.listeners,
56
+ ...EventManager.normalizeListeners(adHocListeners)
57
+ ];
58
+
59
+ for (const listener of allListeners) {
60
+ listener(event);
61
+ }
62
+ }
63
+ }
64
+
65
+ module.exports = {
66
+ EventManager
67
+ };
package/src/schema.js CHANGED
@@ -768,21 +768,28 @@ class SetAttribute extends Attribute {
768
768
  _makeSetValidate(definition) {
769
769
  const validate = this._makeValidate(definition.validate);
770
770
  return (value) => {
771
- if (Array.isArray(value)) {
772
- return validate([...value]);
773
- } else if (value && value.wrapperName === 'Set') {
774
- return validate([...value.values])
775
- } else {
776
- return validate(value);
771
+ switch (getValueType(value)) {
772
+ case ValueTypes.array:
773
+ return validate([...value]);
774
+ case ValueTypes.aws_set:
775
+ return validate([...value.values]);
776
+ case ValueTypes.set:
777
+ return validate(Array.from(value));
778
+ default:
779
+ return validate(value);
777
780
  }
778
781
  }
779
782
  }
780
783
 
781
784
  fromDDBSet(value) {
782
- if (getValueType(value) === ValueTypes.aws_set) {
783
- return value.values;
785
+ switch (getValueType(value)) {
786
+ case ValueTypes.aws_set:
787
+ return [...value.values];
788
+ case ValueTypes.set:
789
+ return Array.from(value);
790
+ default:
791
+ return value;
784
792
  }
785
- return value;
786
793
  }
787
794
 
788
795
  _createDDBSet(value) {
@@ -820,15 +827,14 @@ class SetAttribute extends Attribute {
820
827
 
821
828
  _makeGet(get, items) {
822
829
  this._checkGetSet(get, "get");
823
-
824
830
  const getter = get || ((attr) => attr);
825
-
826
831
  return (values, siblings) => {
827
832
  if (values !== undefined) {
828
833
  const data = this.fromDDBSet(values);
829
834
  return getter(data, siblings);
830
835
  }
831
- let results = getter(data, siblings);
836
+ const data = this.fromDDBSet(values);
837
+ const results = getter(data, siblings);
832
838
  if (results !== undefined) {
833
839
  // if not undefined, try to convert, else no need to return
834
840
  return this.fromDDBSet(results);
@@ -840,9 +846,7 @@ class SetAttribute extends Attribute {
840
846
  this._checkGetSet(set, "set");
841
847
  const setter = set || ((attr) => attr);
842
848
  return (values, siblings) => {
843
- const results = values && values.wrapperName === 'Set'
844
- ? setter(values.values, siblings)
845
- : setter(values, siblings)
849
+ const results = setter(this.fromDDBSet(values), siblings);
846
850
  if (results !== undefined) {
847
851
  return this.toDDBSet(results);
848
852
  }
@@ -879,7 +883,7 @@ class SetAttribute extends Attribute {
879
883
  } else {
880
884
  errors.push(
881
885
  new e.ElectroAttributeValidationError(this.path, `Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"`)
882
- )
886
+ );
883
887
  }
884
888
  for (const item of arr) {
885
889
  const [isValid, errorValues] = this.items.isValid(item);
package/src/service.js CHANGED
@@ -6,6 +6,7 @@ const { FilterOperations } = require("./operations");
6
6
  const { WhereFactory } = require("./where");
7
7
  const { getInstanceType, getModelVersion, applyBetaModelOverrides } = require("./util");
8
8
  const v = require("./validations");
9
+ const c = require('./client');
9
10
  const e = require("./errors");
10
11
  const u = require("./util");
11
12
 
@@ -58,6 +59,9 @@ class Service {
58
59
 
59
60
  this.config = config;
60
61
  this.client = config.client;
62
+ if (v.isFunction(config.logger)) {
63
+ this.logger = config.logger;
64
+ }
61
65
  this.entities = {};
62
66
  this.find = {};
63
67
  this.collectionSchema = {};
@@ -79,6 +83,9 @@ class Service {
79
83
 
80
84
  this.config = config;
81
85
  this.client = config.client;
86
+ if (v.isFunction(config.logger)) {
87
+ this.logger = config.logger;
88
+ }
82
89
  this.entities = {};
83
90
  this.find = {};
84
91
  this.collectionSchema = {};
@@ -98,6 +105,7 @@ class Service {
98
105
  }
99
106
 
100
107
  constructor(service = "", config = {}) {
108
+ config = c.normalizeConfig(config);
101
109
  this.version = ServiceVersions.v1;
102
110
  let type = inferConstructorType(service);
103
111
  switch(type) {
package/src/types.js CHANGED
@@ -179,7 +179,12 @@ const KeyCasing = {
179
179
  upper: "upper",
180
180
  lower: "lower",
181
181
  default: "default",
182
- }
182
+ };
183
+
184
+ const EventSubscriptionTypes = [
185
+ "query",
186
+ "results"
187
+ ];
183
188
 
184
189
  module.exports = {
185
190
  Pager,
@@ -208,5 +213,6 @@ module.exports = {
208
213
  FormatToReturnValues,
209
214
  AttributeProxySymbol,
210
215
  ElectroInstanceTypes,
216
+ EventSubscriptionTypes,
211
217
  AttributeMutationMethods
212
218
  };
package/src/util.js CHANGED
@@ -1,5 +1,6 @@
1
- const v = require("./validations");
2
1
  const t = require("./types");
2
+ const e = require("./errors");
3
+ const v = require("./validations");
3
4
 
4
5
  function parseJSONPath(path = "") {
5
6
  if (typeof path !== "string") {