electrodb 1.6.3 → 1.7.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/CHANGELOG.md CHANGED
@@ -146,4 +146,8 @@ 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.
package/README.md CHANGED
@@ -305,9 +305,9 @@ npm install electrodb --save
305
305
  # Usage
306
306
  Require/import `Entity` and/or `Service` from `electrodb`:
307
307
  ```javascript
308
- const {Entity, Service} = require("electrodb");
308
+ const { Entity, Service } = require("electrodb");
309
309
  // or
310
- import {Entity, Service} from "electrodb";
310
+ import { Entity, Service } from "electrodb";
311
311
  ```
312
312
 
313
313
  # Entities and Services
@@ -325,9 +325,9 @@ In ***ElectroDB*** an `Entity` is represents a single business object. For examp
325
325
 
326
326
  Require or import `Entity` from `electrodb`:
327
327
  ```javascript
328
- const {Entity} = require("electrodb");
328
+ const { Entity } = require("electrodb");
329
329
  // or
330
- import {Entity} from "electrodb";
330
+ import { Entity } from "electrodb";
331
331
  ```
332
332
 
333
333
  > 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 +337,9 @@ In ***ElectroDB*** a `Service` represents a collection of related Entities. Serv
337
337
 
338
338
  Require:
339
339
  ```javascript
340
- const {Service} = require("electrodb");
340
+ const { Service } = require("electrodb");
341
341
  // or
342
- import {Service} from "electrodb";
342
+ import { Service } from "electrodb";
343
343
  ```
344
344
 
345
345
  ## TypeScript Support
@@ -371,7 +371,7 @@ The property name you assign the entity will then be "alias", or name, you can r
371
371
 
372
372
  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
373
 
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.
374
+ While not yet typed, this pattern will also accept Models, or a mix of Entities and Models, in the same object literal format.
375
375
 
376
376
  ## Join
377
377
  When using JavaScript, use `join` to add [Entities](#entities) or [Models](#model) onto a Service.
@@ -4000,7 +4000,7 @@ TaskApp.collections
4000
4000
  ```
4001
4001
 
4002
4002
  ## 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()`).
4003
+ 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
4004
 
4005
4005
  Both `.params()` and `.go()` take a query configuration object which is detailed more in the section [Query Options](#query-options).
4006
4006
 
@@ -4287,6 +4287,8 @@ By default, **ElectroDB** enables you to work with records as the names and prop
4287
4287
  ignoreOwnership?: boolean;
4288
4288
  limit?: number;
4289
4289
  pages?: number;
4290
+ logger?: (event) => void;
4291
+ listeners Array<(event) => void>;
4290
4292
  };
4291
4293
  ```
4292
4294
 
@@ -4304,6 +4306,253 @@ response | `"default"` | Used as a convenience for applying the
4304
4306
  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
4307
  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
4308
  pages | ∞ | How many DynamoDB pages should a query iterate through before stopping. By default ElectroDB paginate through all results for your query.
4309
+ listeners | `[]` | An array of callbacks that are invoked when [internal ElectroDB events](#events) occur.
4310
+ logger | _none_ | A convenience option for a single event listener that semantically can be used for logging.
4311
+
4312
+ # Events
4313
+ 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!
4314
+
4315
+ ## Query Event
4316
+ 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.
4317
+
4318
+ *Type:*
4319
+ ```typescript
4320
+ interface ElectroQueryEvent<P extends any = any> {
4321
+ type: 'query';
4322
+ method: "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
4323
+ config: any;
4324
+ params: P;
4325
+ }
4326
+ ```
4327
+
4328
+ *Example Input:*
4329
+ ```typescript
4330
+ const prop1 = "22874c81-27c4-4264-92c3-b280aa79aa30";
4331
+ const prop2 = "366aade8-a7c0-4328-8e14-0331b185de4e";
4332
+ const prop3 = "3ec9ed0c-7497-4d05-bdb8-86c09a618047";
4333
+
4334
+ entity.update({ prop1, prop2 })
4335
+ .set({ prop3 })
4336
+ .go()
4337
+ ```
4338
+
4339
+ *Example Output:*
4340
+ ```json
4341
+ {
4342
+ "type": "query",
4343
+ "method": "update",
4344
+ "params": {
4345
+ "UpdateExpression": "SET #prop3 = :prop3_u0, #prop1 = :prop1_u0, #prop2 = :prop2_u0, #__edb_e__ = :__edb_e___u0, #__edb_v__ = :__edb_v___u0",
4346
+ "ExpressionAttributeNames": {
4347
+ "#prop3": "prop3",
4348
+ "#prop1": "prop1",
4349
+ "#prop2": "prop2",
4350
+ "#__edb_e__": "__edb_e__",
4351
+ "#__edb_v__": "__edb_v__"
4352
+ },
4353
+ "ExpressionAttributeValues": {
4354
+ ":prop3_u0": "3ec9ed0c-7497-4d05-bdb8-86c09a618047",
4355
+ ":prop1_u0": "22874c81-27c4-4264-92c3-b280aa79aa30",
4356
+ ":prop2_u0": "366aade8-a7c0-4328-8e14-0331b185de4e",
4357
+ ":__edb_e___u0": "entity",
4358
+ ":__edb_v___u0": "1"
4359
+ },
4360
+ "TableName": "electro",
4361
+ "Key": {
4362
+ "pk": "$test#prop1_22874c81-27c4-4264-92c3-b280aa79aa30",
4363
+ "sk": "$testcollection#entity_1#prop2_366aade8-a7c0-4328-8e14-0331b185de4e"
4364
+ }
4365
+ },
4366
+ "config": { }
4367
+ }
4368
+ ```
4369
+
4370
+ ## Results Event
4371
+ 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.
4372
+
4373
+ > **Pro-Tip:**
4374
+ > 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.
4375
+
4376
+ *Type::*
4377
+ ```typescript
4378
+ interface ElectroResultsEvent<R extends any = any> {
4379
+ type: 'results';
4380
+ method: "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
4381
+ config: any;
4382
+ results: R;
4383
+ success: boolean;
4384
+ }
4385
+ ```
4386
+
4387
+ *Example Input:*
4388
+ ```typescript
4389
+ const prop1 = "22874c81-27c4-4264-92c3-b280aa79aa30";
4390
+ const prop2 = "366aade8-a7c0-4328-8e14-0331b185de4e";
4391
+
4392
+ entity.get({ prop1, prop2 }).go();
4393
+ ```
4394
+
4395
+ *Example Output:*
4396
+ ```typescript
4397
+ {
4398
+ "type": "results",
4399
+ "method": "get",
4400
+ "config": { },
4401
+ "success": true,
4402
+ "results": {
4403
+ "Item": {
4404
+ "prop2": "366aade8-a7c0-4328-8e14-0331b185de4e",
4405
+ "sk": "$testcollection#entity_1#prop2_366aade8-a7c0-4328-8e14-0331b185de4e",
4406
+ "prop1": "22874c81-27c4-4264-92c3-b280aa79aa30",
4407
+ "prop3": "3ec9ed0c-7497-4d05-bdb8-86c09a618047",
4408
+ "__edb_e__": "entity",
4409
+ "__edb_v__": "1",
4410
+ "pk": "$test_1#prop1_22874c81-27c4-4264-92c3-b280aa79aa30"
4411
+ }
4412
+ }
4413
+ }
4414
+ ```
4415
+
4416
+ # Logging
4417
+ 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).
4418
+
4419
+ *On the instantiation of an `Entity`:*
4420
+ ```typescript
4421
+ import { DynamoDB } from 'aws-sdk';
4422
+ import {Entity, ElectroEvent} from 'electrodb';
4423
+
4424
+ const table = "my_table_name";
4425
+ const client = new DynamoDB.DocumentClient();
4426
+ const logger = (event: ElectroEvent) => {
4427
+ console.log(JSON.stringify(event, null, 4));
4428
+ }
4429
+
4430
+ const task = new Entity({
4431
+ // your model
4432
+ }, {
4433
+ client,
4434
+ table,
4435
+ logger // <----- logger listener
4436
+ });
4437
+ ```
4438
+
4439
+ *On the instantiation of an `Service`:*
4440
+ ```typescript
4441
+ import { DynamoDB } from 'aws-sdk';
4442
+ import {Entity, ElectroEvent} from 'electrodb';
4443
+
4444
+ const table = "my_table_name";
4445
+ const client = new DynamoDB.DocumentClient();
4446
+ const logger = (event: ElectroEvent) => {
4447
+ console.log(JSON.stringify(event, null, 4));
4448
+ }
4449
+
4450
+ const task = new Entity({
4451
+ // your model
4452
+ });
4453
+
4454
+ const user = new Entity({
4455
+ // your model
4456
+ });
4457
+
4458
+ const service = new Service({ task, user }, {
4459
+ client,
4460
+ table,
4461
+ logger // <----- logger listener
4462
+ });
4463
+ ```
4464
+
4465
+ *As a [Query Option](#query-options):*
4466
+ ```typescript
4467
+ const logger = (event: ElectroEvent) => {
4468
+ console.log(JSON.stringify(event, null, 4));
4469
+ }
4470
+
4471
+ task.query
4472
+ .assigned({ userId })
4473
+ .go({ logger });
4474
+ ```
4475
+
4476
+ # Listeners
4477
+ 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).
4478
+
4479
+ > _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.
4480
+
4481
+ *On the instantiation of an `Entity`:*
4482
+ ```typescript
4483
+ import { DynamoDB } from 'aws-sdk';
4484
+ import {Entity, ElectroEvent} from 'electrodb';
4485
+
4486
+ const table = "my_table_name";
4487
+ const client = new DynamoDB.DocumentClient();
4488
+ const listener1 = (event: ElectroEvent) => {
4489
+ // do work
4490
+ }
4491
+
4492
+ const listener2 = (event: ElectroEvent) => {
4493
+ // do work
4494
+ }
4495
+
4496
+ const task = new Entity({
4497
+ // your model
4498
+ }, {
4499
+ client,
4500
+ table,
4501
+ listeners: [
4502
+ listener1,
4503
+ listener2, // <----- supports multiple listeners
4504
+ ]
4505
+ });
4506
+ ```
4507
+
4508
+ *On the instantiation of an `Service`:*
4509
+ ```typescript
4510
+ import { DynamoDB } from 'aws-sdk';
4511
+ import {Entity, ElectroEvent} from 'electrodb';
4512
+
4513
+ const table = "my_table_name";
4514
+ const client = new DynamoDB.DocumentClient();
4515
+
4516
+ const listener1 = (event: ElectroEvent) => {
4517
+ // do work
4518
+ }
4519
+
4520
+ const listener2 = (event: ElectroEvent) => {
4521
+ // do work
4522
+ }
4523
+
4524
+ const task = new Entity({
4525
+ // your model
4526
+ });
4527
+
4528
+ const user = new Entity({
4529
+ // your model
4530
+ });
4531
+
4532
+ const service = new Service({ task, user }, {
4533
+ client,
4534
+ table,
4535
+ listeners: [
4536
+ listener1,
4537
+ listener2, // <----- supports multiple listeners
4538
+ ]
4539
+ });
4540
+ ```
4541
+
4542
+ *As a [Query Option](#query-options):*
4543
+ ```typescript
4544
+ const listener1 = (event: ElectroEvent) => {
4545
+ // do work
4546
+ }
4547
+
4548
+ const listener2 = (event: ElectroEvent) => {
4549
+ // do work
4550
+ }
4551
+
4552
+ task.query
4553
+ .assigned({ userId })
4554
+ .go({ listeners: [listener1, listener2] });
4555
+ ```
4307
4556
 
4308
4557
  # Errors:
4309
4558
 
@@ -4311,7 +4560,7 @@ Error Code | Description
4311
4560
  :--------: | --------------------
4312
4561
  1000s | Configuration Errors
4313
4562
  2000s | Invalid Queries
4314
- 3000s | User Defined Errors
4563
+ 3000s | User Defined Errors
4315
4564
  4000s | DynamoDB Errors
4316
4565
  5000s | Unexpected Errors
4317
4566
 
package/index.d.ts CHANGED
@@ -974,6 +974,8 @@ interface QueryOptions {
974
974
  originalErr?: boolean;
975
975
  ignoreOwnership?: boolean;
976
976
  pages?: number;
977
+ listeners?: Array<EventListener>;
978
+ logger?: EventListener;
977
979
  }
978
980
 
979
981
  // subset of QueryOptions
@@ -1120,21 +1122,73 @@ type DocumentClient = {
1120
1122
  scan: DocumentClientMethod;
1121
1123
  }
1122
1124
 
1125
+ type ElectroDBMethodTypes = "put" | "get" | "query" | "scan" | "update" | "delete" | "remove" | "patch" | "create" | "batchGet" | "batchWrite";
1126
+
1127
+ interface ElectroQueryEvent<P extends any = any> {
1128
+ type: 'query';
1129
+ method: ElectroDBMethodTypes;
1130
+ config: any;
1131
+ params: P;
1132
+ }
1133
+
1134
+ interface ElectroResultsEvent<R extends any = any> {
1135
+ type: 'results';
1136
+ method: ElectroDBMethodTypes;
1137
+ config: any;
1138
+ results: R;
1139
+ success: boolean;
1140
+ }
1141
+
1142
+ type ElectroEvent =
1143
+ ElectroQueryEvent
1144
+ | ElectroResultsEvent;
1145
+
1146
+ type ElectroEventType = Pick<ElectroEvent, 'type'>;
1147
+
1148
+ type EventListener = (event: ElectroEvent) => void;
1149
+
1150
+ // todo: coming soon, more events!
1151
+ // | {
1152
+ // name: "error";
1153
+ // type: "configuration_error" | "invalid_query" | "dynamodb_client";
1154
+ // message: string;
1155
+ // details: ElectroError;
1156
+ // } | {
1157
+ // name: "error";
1158
+ // type: "user_defined";
1159
+ // message: string;
1160
+ // details: ElectroValidationError;
1161
+ // } | {
1162
+ // name: "warn";
1163
+ // type: "deprecation_warning" | "optimization_suggestion";
1164
+ // message: string;
1165
+ // details: any;
1166
+ // } | {
1167
+ // name: "info";
1168
+ // type: "client_updated" | "table_overwritten";
1169
+ // message: string;
1170
+ // details: any;
1171
+ // };
1172
+
1123
1173
  type EntityConfiguration = {
1124
1174
  table?: string;
1125
- client?: DocumentClient
1175
+ client?: DocumentClient;
1176
+ listeners?: Array<EventListener>;
1177
+ logger?: EventListener;
1126
1178
  };
1127
1179
 
1128
1180
  type ServiceConfiguration = {
1129
1181
  table?: string;
1130
- client?: DocumentClient
1182
+ client?: DocumentClient;
1183
+ listeners?: Array<EventListener>;
1184
+ logger?: EventListener;
1131
1185
  };
1132
1186
 
1133
1187
  type ParseSingleInput = {
1134
- Item?: {[key: string]: any}
1135
- } | {
1136
- Attributes?: {[key: string]: any}
1137
- } | null
1188
+ Item?: {[key: string]: any}
1189
+ } | {
1190
+ Attributes?: {[key: string]: any}
1191
+ } | null;
1138
1192
 
1139
1193
  type ParseMultiInput = {
1140
1194
  Items?: {[key: string]: any}[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "1.6.3",
3
+ "version": "1.7.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/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/entity.js CHANGED
@@ -5,12 +5,17 @@ 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 u = require("./util");
10
11
  const e = require("./errors");
11
12
 
12
13
  class Entity {
13
14
  constructor(model, config = {}) {
15
+ this.eventManager = new EventManager({
16
+ listeners: config.listeners
17
+ });
18
+ this.eventManager.add(config.logger);
14
19
  this._validateModel(model);
15
20
  this.version = EntityVersions.v1;
16
21
  this.config = config;
@@ -43,7 +48,7 @@ class Entity {
43
48
 
44
49
  setIdentifier(type = "", identifier = "") {
45
50
  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))}`);
51
+ throw new e.ElectroError(e.ErrorCodes.InvalidIdentifier, `Invalid identifier type: "${type}". Valid identifiers include: ${u.commaSeparatedString(Object.keys(this.identifiers))}`);
47
52
  } else {
48
53
  this.identifiers[type] = identifier;
49
54
  }
@@ -252,9 +257,32 @@ class Entity {
252
257
  }
253
258
  }
254
259
 
255
- async _exec(method, parameters) {
256
- return this.client[method](parameters).promise()
260
+ async _exec(method, params, config = {}) {
261
+ const notifyQuery = () => {
262
+ this.eventManager.trigger({
263
+ type: "query",
264
+ method,
265
+ params,
266
+ config,
267
+ }, config.listeners);
268
+ };
269
+ const notifyResults = (results, success) => {
270
+ this.eventManager.trigger({
271
+ type: "results",
272
+ method,
273
+ config,
274
+ success,
275
+ results,
276
+ }, config.listeners);
277
+ }
278
+ return this.client[method](params).promise()
279
+ .then((results) => {
280
+ notifyQuery();
281
+ notifyResults(results, true);
282
+ return results;
283
+ })
257
284
  .catch(err => {
285
+ notifyResults(err, false);
258
286
  err.__isAWSError = true;
259
287
  throw err;
260
288
  });
@@ -266,10 +294,10 @@ class Entity {
266
294
  }
267
295
  let results = [];
268
296
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
269
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
297
+ let concurrentOperations = u.batchItems(parameters, concurrent);
270
298
  for (let operation of concurrentOperations) {
271
299
  await Promise.all(operation.map(async params => {
272
- let response = await this._exec(MethodTypes.batchWrite, params);
300
+ let response = await this._exec(MethodTypes.batchWrite, params, config);
273
301
  if (validations.isFunction(config.parse)) {
274
302
  let parsed = await config.parse(config, response);
275
303
  if (parsed) {
@@ -292,13 +320,13 @@ class Entity {
292
320
  parameters = [parameters];
293
321
  }
294
322
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
295
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
323
+ let concurrentOperations = u.batchItems(parameters, concurrent);
296
324
 
297
325
  let resultsAll = [];
298
326
  let unprocessedAll = [];
299
327
  for (let operation of concurrentOperations) {
300
328
  await Promise.all(operation.map(async params => {
301
- let response = await this._exec(MethodTypes.batchGet, params);
329
+ let response = await this._exec(MethodTypes.batchGet, params, config);
302
330
  if (validations.isFunction(config.parse)) {
303
331
  resultsAll.push(await config.parse(config, response));
304
332
  return;
@@ -328,7 +356,7 @@ class Entity {
328
356
  let limit = max === undefined
329
357
  ? parameters.Limit
330
358
  : max - count;
331
- let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit});
359
+ let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit}, config);
332
360
 
333
361
  ExclusiveStartKey = response.LastEvaluatedKey;
334
362
 
@@ -363,7 +391,7 @@ class Entity {
363
391
  }
364
392
 
365
393
  async executeOperation(method, parameters, config) {
366
- let response = await this._exec(method, parameters);
394
+ let response = await this._exec(method, parameters, config);
367
395
  if (validations.isFunction(config.parse)) {
368
396
  return config.parse(config, response);
369
397
  }
@@ -751,13 +779,14 @@ class Entity {
751
779
  _isPagination: false,
752
780
  _isCollectionQuery: false,
753
781
  pages: undefined,
782
+ listeners: [],
754
783
  };
755
784
 
756
785
  config = options.reduce((config, option) => {
757
786
  if (typeof option.response === 'string' && option.response.length) {
758
787
  const format = ReturnValues[option.response];
759
788
  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))}.`);
789
+ 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
790
  }
762
791
  config.response = format;
763
792
  config.params.ReturnValues = FormatToReturnValues[format];
@@ -814,7 +843,7 @@ class Entity {
814
843
  if (typeof Pager[option.pager] === "string") {
815
844
  config.pager = option.pager;
816
845
  } 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))}.`);
846
+ throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "pager" provided: "${option.pager}". Allowed values include ${u.commaSeparatedString(Object.keys(Pager))}.`);
818
847
  }
819
848
  }
820
849
 
@@ -822,7 +851,7 @@ class Entity {
822
851
  if (typeof UnprocessedTypes[option.unprocessed] === "string") {
823
852
  config.unproessed = UnprocessedTypes[option.unprocessed];
824
853
  } 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))}.`);
854
+ throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Invalid value for option "unprocessed" provided: "${option.unprocessed}". Allowed values include ${u.commaSeparatedString(Object.keys(UnprocessedTypes))}.`);
826
855
  }
827
856
  }
828
857
 
@@ -830,6 +859,20 @@ class Entity {
830
859
  config.ignoreOwnership = option.ignoreOwnership;
831
860
  }
832
861
 
862
+ if (option.listeners) {
863
+ if (Array.isArray(option.listeners)) {
864
+ config.listeners = config.listeners.concat(option.listeners);
865
+ }
866
+ }
867
+
868
+ if (option.logger) {
869
+ if (validations.isFunction(option.logger)) {
870
+ config.listeners.push(option.logger);
871
+ } else {
872
+ throw new e.ElectroError(e.ErrorCodes.InvalidLoggerProvided, `Loggers must be of type function`);
873
+ }
874
+ }
875
+
833
876
  config.page = Object.assign({}, config.page, option.page);
834
877
  config.params = Object.assign({}, config.params, option.params);
835
878
  return config;
@@ -933,7 +976,7 @@ class Entity {
933
976
  records.push(Key);
934
977
  }
935
978
  }
936
- let batches = utilities.batchItems(records, MaxBatchItems.batchGet);
979
+ let batches = u.batchItems(records, MaxBatchItems.batchGet);
937
980
  return batches.map(batch => {
938
981
  return {
939
982
  RequestItems: {
@@ -966,7 +1009,7 @@ class Entity {
966
1009
  throw new Error("Invalid method type");
967
1010
  }
968
1011
  }
969
- let batches = utilities.batchItems(records, MaxBatchItems.batchWrite);
1012
+ let batches = u.batchItems(records, MaxBatchItems.batchWrite);
970
1013
  return batches.map(batch => {
971
1014
  return {
972
1015
  RequestItems: {
@@ -1220,7 +1263,7 @@ class Entity {
1220
1263
  let props = Object.keys(item);
1221
1264
  let missing = require.filter((prop) => !props.includes(prop));
1222
1265
  if (!missing) {
1223
- throw new e.ElectroError(e.ErrorCodes.MissingAttribute, `Item is missing attributes: ${utilities.commaSeparatedString(missing)}`);
1266
+ throw new e.ElectroError(e.ErrorCodes.MissingAttribute, `Item is missing attributes: ${u.commaSeparatedString(missing)}`);
1224
1267
  }
1225
1268
  }
1226
1269
 
@@ -1229,7 +1272,7 @@ class Entity {
1229
1272
  throw new Error(`Invalid attribute ${prop}`);
1230
1273
  }
1231
1274
  if (restrict.length && !restrict.includes(prop)) {
1232
- throw new Error(`${prop} is not a valid attribute: ${utilities.commaSeparatedString(restrict)}`);
1275
+ throw new Error(`${prop} is not a valid attribute: ${u.commaSeparatedString(restrict)}`);
1233
1276
  }
1234
1277
  if (prop === undefined || skip.includes(prop)) {
1235
1278
  continue;
@@ -1392,7 +1435,7 @@ class Entity {
1392
1435
  _makeComparisonQueryParams(index = TableIndex, comparison = "", filter = {}, pk = {}, sk = {}) {
1393
1436
  let operator = Comparisons[comparison];
1394
1437
  if (!operator) {
1395
- throw new Error(`Unexpected comparison operator "${comparison}", expected ${utilities.commaSeparatedString(Object.values(Comparisons))}`);
1438
+ throw new Error(`Unexpected comparison operator "${comparison}", expected ${u.commaSeparatedString(Object.values(Comparisons))}`);
1396
1439
  }
1397
1440
  let keyExpressions = this._queryKeyExpressionAttributeBuilder(
1398
1441
  index,
@@ -1429,7 +1472,7 @@ class Entity {
1429
1472
  let incompleteAccessPatterns = incomplete.map(({index}) => this.model.translations.indexes.fromIndexToAccessPattern[index]);
1430
1473
  let missingFacets = incomplete.reduce((result, { missing }) => [...result, ...missing], []);
1431
1474
  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))} `,
1475
+ `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
1476
  );
1434
1477
  }
1435
1478
  return complete;
@@ -1648,7 +1691,7 @@ class Entity {
1648
1691
  _expectFacets(obj = {}, properties = [], type = "key composite attributes") {
1649
1692
  let [incompletePk, missing, matching] = this._expectProperties(obj, properties);
1650
1693
  if (incompletePk) {
1651
- throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes, `Incomplete or invalid ${type} supplied. Missing properties: ${utilities.commaSeparatedString(missing)}`);
1694
+ throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes, `Incomplete or invalid ${type} supplied. Missing properties: ${u.commaSeparatedString(missing)}`);
1652
1695
  } else {
1653
1696
  return matching;
1654
1697
  }
@@ -1721,10 +1764,10 @@ class Entity {
1721
1764
 
1722
1765
  // If keys arent custom, set the prefixes
1723
1766
  if (!keys.pk.isCustom) {
1724
- keys.pk.prefix = utilities.formatKeyCasing(pk, tableIndex.pk.casing);
1767
+ keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
1725
1768
  }
1726
1769
  if (!keys.sk.isCustom) {
1727
- keys.sk.prefix = utilities.formatKeyCasing(sk, tableIndex.sk.casing);
1770
+ keys.sk.prefix = u.formatKeyCasing(sk, tableIndex.sk.casing);
1728
1771
  }
1729
1772
 
1730
1773
  return keys;
@@ -1735,7 +1778,7 @@ class Entity {
1735
1778
  ? this.model.indexes[accessPattern].sk.casing
1736
1779
  : undefined;
1737
1780
 
1738
- return utilities.formatKeyCasing(key, casing);
1781
+ return u.formatKeyCasing(key, casing);
1739
1782
  }
1740
1783
 
1741
1784
  _validateIndex(index) {
@@ -1859,7 +1902,7 @@ class Entity {
1859
1902
  key = `${key}${supplied[name]}`;
1860
1903
  }
1861
1904
 
1862
- return utilities.formatKeyCasing(key, casing);
1905
+ return u.formatKeyCasing(key, casing);
1863
1906
  }
1864
1907
 
1865
1908
  _findBestIndexKeyMatch(attributes = {}) {
@@ -2139,7 +2182,7 @@ class Entity {
2139
2182
  let indexName = index.index || TableIndex;
2140
2183
  if (seenIndexes[indexName] !== undefined) {
2141
2184
  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.`);
2185
+ 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
2186
  } else {
2144
2187
  throw new e.ElectroError(e.ErrorCodes.DuplicateIndexes, `Duplicate index defined in model found in Access Pattern '${accessPattern}': '${indexName}'`);
2145
2188
  }
@@ -2148,7 +2191,7 @@ class Entity {
2148
2191
  let hasSk = !!index.sk;
2149
2192
  let inCollection = !!index.collection;
2150
2193
  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.`);
2194
+ 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
2195
  }
2153
2196
  let collection = index.collection || "";
2154
2197
  let customFacets = {
@@ -2219,7 +2262,7 @@ class Entity {
2219
2262
  if (Array.isArray(sk.facets)) {
2220
2263
  let duplicates = pk.facets.filter(facet => sk.facets.includes(facet));
2221
2264
  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.`);
2265
+ 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
2266
  }
2224
2267
  }
2225
2268
 
@@ -2322,13 +2365,13 @@ class Entity {
2322
2365
 
2323
2366
  let pkTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedPKAttributes, index.pk.composite);
2324
2367
  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)}`);
2368
+ 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
2369
  }
2327
2370
 
2328
2371
  if (index.sk !== undefined && Array.isArray(index.sk.composite) && typeof index.sk.template === "string") {
2329
2372
  let skTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedSKAttributes, index.sk.composite);
2330
2373
  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)}`);
2374
+ 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
2375
  }
2333
2376
  }
2334
2377
  }
@@ -2356,7 +2399,7 @@ class Entity {
2356
2399
 
2357
2400
  for (let [name, fn] of Object.entries(filters)) {
2358
2401
  if (invalidFilterNames.includes(name)) {
2359
- throw new e.ElectroError(e.ErrorCodes.InvalidFilter, `Invalid filter name: ${name}. Filter cannot be named ${utilities.commaSeparatedString(invalidFilterNames)}`);
2402
+ throw new e.ElectroError(e.ErrorCodes.InvalidFilter, `Invalid filter name: ${name}. Filter cannot be named ${u.commaSeparatedString(invalidFilterNames)}`);
2360
2403
  } else {
2361
2404
  normalized[name] = fn;
2362
2405
  }
@@ -2475,7 +2518,7 @@ class Entity {
2475
2518
  _parseModel(model, config = {}) {
2476
2519
  /** start beta/v1 condition **/
2477
2520
  const {client} = config;
2478
- let modelVersion = utilities.getModelVersion(model);
2521
+ let modelVersion = u.getModelVersion(model);
2479
2522
  let service, entity, version, table, name;
2480
2523
  switch(modelVersion) {
2481
2524
  case ModelVersions.beta:
package/src/errors.js CHANGED
@@ -127,6 +127,12 @@ 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
+ },
130
136
  MissingAttribute: {
131
137
  code: 2001,
132
138
  section: "missing-attribute",
@@ -204,7 +210,7 @@ const ErrorCodes = {
204
210
  section: "pager-not-unique",
205
211
  name: "NoOwnerForPager",
206
212
  sym: ErrorCode,
207
- },
213
+ }
208
214
  };
209
215
 
210
216
  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/service.js CHANGED
@@ -58,6 +58,9 @@ class Service {
58
58
 
59
59
  this.config = config;
60
60
  this.client = config.client;
61
+ if (v.isFunction(config.logger)) {
62
+ this.logger = config.logger;
63
+ }
61
64
  this.entities = {};
62
65
  this.find = {};
63
66
  this.collectionSchema = {};
@@ -79,6 +82,9 @@ class Service {
79
82
 
80
83
  this.config = config;
81
84
  this.client = config.client;
85
+ if (v.isFunction(config.logger)) {
86
+ this.logger = config.logger;
87
+ }
82
88
  this.entities = {};
83
89
  this.find = {};
84
90
  this.collectionSchema = {};
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") {