electrodb 1.6.1 → 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
@@ -138,4 +138,16 @@ All notable changes to this project will be documented in this file. Breaking ch
138
138
 
139
139
  ## [1.6.1] - 2021-12-05
140
140
  ### Fixed
141
- - In some cases the `find()` and `match()` methods would incorrectly select an index without a complete partition key. This would result in validation exceptions preventing the user from querying if an index definition and provided attribute object aligned improperly. This was fixed and a slightly more robust mechanism for ranking indexes was made.
141
+ - In some cases the `find()` and `match()` methods would incorrectly select an index without a complete partition key. This would result in validation exceptions preventing the user from querying if an index definition and provided attribute object aligned improperly. This was fixed and a slightly more robust mechanism for ranking indexes was made.
142
+
143
+ ## [1.6.2] - 2022-01-27
144
+ ### Changed
145
+ - The methods `create`, `patch`, and `remove` will now refer to primary table keys through parameters via ExpressionAttributeNames when using `attribute_exists()`/`attribute_not_exists()` DynamoDB conditions. Prior to this they were referenced directly which would fail in cases where key names include illegal characters. Parameter implementation change only, non-breaking.
146
+
147
+ ## [1.6.3] - 2022-02-22
148
+ ### Added
149
+ - Add `data` update operation `ifNotExists` to allow for use of the UpdateExpression function "if_not_exists()".
150
+
151
+ ## [1.7.0] - 2022-03-13
152
+ ### Added
153
+ - New feature: "Listeners". Listeners open the door to some really cool tooling that was not possible because of how ElectroDB augments raw DynamoDB responses and did not provide easy access to raw DyanmoDB parameters.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![Coverage Status](https://coveralls.io/repos/github/tywalch/electrodb/badge.svg?branch=master)](https://coveralls.io/github/tywalch/electrodb?branch=master&kill_cache=please)
3
3
  [![Coverage Status](https://img.shields.io/npm/dt/electrodb.svg)](https://www.npmjs.com/package/electrodb)
4
4
  ![npm bundle size](https://img.shields.io/bundlephobia/min/electrodb) [![Build Status](https://travis-ci.org/tywalch/electrodb.svg?branch=master)](https://travis-ci.org/tywalch/electrodb)
5
- [![Runkit Demo](https://img.shields.io/badge/runkit-electrodb-db4792)](https://runkit.com/tywalch/creating-and-querying-an-electrodb-service)
5
+ [![Runkit Demo](https://img.shields.io/badge/runkit-electrodb-db4792)](https://runkit.com/tywalch/electrodb-building-queries)
6
6
 
7
7
  ![ElectroDB](https://github.com/tywalch/electrodb/blob/master/assets/electrodb-drk.png?raw=true)
8
8
  ***ElectroDB*** is a DynamoDB library to ease the use of having multiple entities and complex hierarchical relationships in a single DynamoDB table.
@@ -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.
@@ -3462,7 +3462,8 @@ operation | example | result
3462
3462
  `delete` | `delete(tenant, name)` | `#tenant :tenant1` | Remove item from existing `set` attribute
3463
3463
  `del` | `del(tenant, name)` | `#tenant :tenant1` | Alias for `delete` operation
3464
3464
  `name` | `name(rent)` | `#rent` | Reference another attribute's name, can be passed to other operation that allows leveraging existing attribute values in calculating new values
3465
- `value` | `value(rent, value)` | `:rent1` | Create a reference to a particular value, can be passed to other operation that allows leveraging existing attribute values in calculating new values
3465
+ `value` | `value(rent, amount)` | `:rent1` | Create a reference to a particular value, can be passed to other operation that allows leveraging existing attribute values in calculating new values
3466
+ `ifNotExists` | `ifNotExists(rent, amount)` | `#rent = if_not_exists(#rent, :rent0)` | Update a property's value only if that property doesn't yet exist on the record
3466
3467
 
3467
3468
  ```javascript
3468
3469
  await StoreLocations
@@ -3999,7 +4000,7 @@ TaskApp.collections
3999
4000
  ```
4000
4001
 
4001
4002
  ## Execute Queries
4002
- 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()`).
4003
4004
 
4004
4005
  Both `.params()` and `.go()` take a query configuration object which is detailed more in the section [Query Options](#query-options).
4005
4006
 
@@ -4286,6 +4287,8 @@ By default, **ElectroDB** enables you to work with records as the names and prop
4286
4287
  ignoreOwnership?: boolean;
4287
4288
  limit?: number;
4288
4289
  pages?: number;
4290
+ logger?: (event) => void;
4291
+ listeners Array<(event) => void>;
4289
4292
  };
4290
4293
  ```
4291
4294
 
@@ -4303,6 +4306,253 @@ response | `"default"` | Used as a convenience for applying the
4303
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`.
4304
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.
4305
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
+ ```
4306
4556
 
4307
4557
  # Errors:
4308
4558
 
@@ -4310,7 +4560,7 @@ Error Code | Description
4310
4560
  :--------: | --------------------
4311
4561
  1000s | Configuration Errors
4312
4562
  2000s | Invalid Queries
4313
- 3000s | User Defined Errors
4563
+ 3000s | User Defined Errors
4314
4564
  4000s | DynamoDB Errors
4315
4565
  5000s | Unexpected Errors
4316
4566
 
package/index.d.ts CHANGED
@@ -954,6 +954,7 @@ type DataUpdateOperations<A extends string, F extends A, C extends string, S ext
954
954
  del: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: A extends DataUpdateAttributeSymbol<infer V> ? V extends Array<any> ? V : never : never ) => any;
955
955
  value: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: DataUpdateAttributeValues<A>) => Required<DataUpdateAttributeValues<A>>;
956
956
  name: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A) => any;
957
+ ifNotExists: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: DataUpdateAttributeValues<A>) => any;
957
958
  };
958
959
 
959
960
  type WhereCallback<A extends string, F extends A, C extends string, S extends Schema<A,F,C>, I extends Item<A,F,C,S,S["attributes"]>> =
@@ -973,6 +974,8 @@ interface QueryOptions {
973
974
  originalErr?: boolean;
974
975
  ignoreOwnership?: boolean;
975
976
  pages?: number;
977
+ listeners?: Array<EventListener>;
978
+ logger?: EventListener;
976
979
  }
977
980
 
978
981
  // subset of QueryOptions
@@ -1119,21 +1122,73 @@ type DocumentClient = {
1119
1122
  scan: DocumentClientMethod;
1120
1123
  }
1121
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
+
1122
1173
  type EntityConfiguration = {
1123
1174
  table?: string;
1124
- client?: DocumentClient
1175
+ client?: DocumentClient;
1176
+ listeners?: Array<EventListener>;
1177
+ logger?: EventListener;
1125
1178
  };
1126
1179
 
1127
1180
  type ServiceConfiguration = {
1128
1181
  table?: string;
1129
- client?: DocumentClient
1182
+ client?: DocumentClient;
1183
+ listeners?: Array<EventListener>;
1184
+ logger?: EventListener;
1130
1185
  };
1131
1186
 
1132
1187
  type ParseSingleInput = {
1133
- Item?: {[key: string]: any}
1134
- } | {
1135
- Attributes?: {[key: string]: any}
1136
- } | null
1188
+ Item?: {[key: string]: any}
1189
+ } | {
1190
+ Attributes?: {[key: string]: any}
1191
+ } | null;
1137
1192
 
1138
1193
  type ParseMultiInput = {
1139
1194
  Items?: {[key: string]: any}[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "1.6.1",
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
@@ -1,5 +1,5 @@
1
- const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes } = require("./types");
2
- const {AttributeOperationProxy, UpdateOperations} = require("./operations");
1
+ const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes, TableIndex } = require("./types");
2
+ const {AttributeOperationProxy, UpdateOperations, FilterOperationNames} = require("./operations");
3
3
  const {UpdateExpression} = require("./update");
4
4
  const {FilterExpression} = require("./where");
5
5
  const v = require("./validations");
@@ -134,6 +134,12 @@ let clauses = {
134
134
  }
135
135
  try {
136
136
  const attributes = state.getCompositeAttributes();
137
+ const filter = state.query.filter[ExpressionTypes.ConditionExpression];
138
+ const {pk, sk} = entity._getPrimaryIndexFieldNames();
139
+ filter.unsafeSet(FilterOperationNames.exists, pk);
140
+ if (sk) {
141
+ filter.unsafeSet(FilterOperationNames.exists, sk);
142
+ }
137
143
  return state
138
144
  .setMethod(MethodTypes.delete)
139
145
  .setType(QueryTypes.eq)
@@ -189,6 +195,12 @@ let clauses = {
189
195
  try {
190
196
  let record = entity.model.schema.checkCreate({...payload});
191
197
  const attributes = state.getCompositeAttributes();
198
+ const filter = state.query.filter[ExpressionTypes.ConditionExpression];
199
+ const {pk, sk} = entity._getPrimaryIndexFieldNames();
200
+ filter.unsafeSet(FilterOperationNames.notExists, pk);
201
+ if (sk) {
202
+ filter.unsafeSet(FilterOperationNames.notExists, sk);
203
+ }
192
204
  return state
193
205
  .setMethod(MethodTypes.put)
194
206
  .setType(QueryTypes.eq)
@@ -213,6 +225,12 @@ let clauses = {
213
225
  }
214
226
  try {
215
227
  const attributes = state.getCompositeAttributes();
228
+ const filter = state.query.filter[ExpressionTypes.ConditionExpression];
229
+ const {pk, sk} = entity._getPrimaryIndexFieldNames();
230
+ filter.unsafeSet(FilterOperationNames.exists, pk);
231
+ if (sk) {
232
+ filter.unsafeSet(FilterOperationNames.exists, sk);
233
+ }
216
234
  return state
217
235
  .setMethod(MethodTypes.update)
218
236
  .setType(QueryTypes.eq)
@@ -522,8 +540,9 @@ let clauses = {
522
540
  if (!v.isStringHasLength(options.table) && !v.isStringHasLength(entity._getTableName())) {
523
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.`);
524
542
  }
543
+ const method = state.getMethod();
525
544
  let results;
526
- switch (state.getMethod()) {
545
+ switch (method) {
527
546
  case MethodTypes.query:
528
547
  results = entity._queryParams(state, options);
529
548
  break;
@@ -538,7 +557,7 @@ let clauses = {
538
557
  break;
539
558
  }
540
559
 
541
- 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) {
542
561
  // An update that only does a `remove` operation would result in an empty object
543
562
  // todo: change the getValues() method to return undefined in this case (would potentially require a more generous refactor)
544
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
  }
@@ -198,11 +203,7 @@ class Entity {
198
203
 
199
204
  create(attributes = {}) {
200
205
  let index = TableIndex;
201
- let options = {
202
- params: {
203
- ConditionExpression: this._makeItemDoesntExistConditions(index)
204
- }
205
- };
206
+ let options = {};
206
207
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).create(attributes);
207
208
  }
208
209
 
@@ -213,21 +214,13 @@ class Entity {
213
214
 
214
215
  patch(facets = {}) {
215
216
  let index = TableIndex;
216
- let options = {
217
- params: {
218
- ConditionExpression: this._makeItemExistsConditions(index)
219
- }
220
- };
217
+ let options = {};
221
218
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).patch(facets);
222
219
  }
223
220
 
224
221
  remove(facets = {}) {
225
222
  let index = TableIndex;
226
- let options = {
227
- params: {
228
- ConditionExpression: this._makeItemExistsConditions(index)
229
- }
230
- };
223
+ let options = {};
231
224
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).remove(facets);
232
225
  }
233
226
 
@@ -264,9 +257,32 @@ class Entity {
264
257
  }
265
258
  }
266
259
 
267
- async _exec(method, parameters) {
268
- 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
+ })
269
284
  .catch(err => {
285
+ notifyResults(err, false);
270
286
  err.__isAWSError = true;
271
287
  throw err;
272
288
  });
@@ -278,10 +294,10 @@ class Entity {
278
294
  }
279
295
  let results = [];
280
296
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
281
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
297
+ let concurrentOperations = u.batchItems(parameters, concurrent);
282
298
  for (let operation of concurrentOperations) {
283
299
  await Promise.all(operation.map(async params => {
284
- let response = await this._exec(MethodTypes.batchWrite, params);
300
+ let response = await this._exec(MethodTypes.batchWrite, params, config);
285
301
  if (validations.isFunction(config.parse)) {
286
302
  let parsed = await config.parse(config, response);
287
303
  if (parsed) {
@@ -304,13 +320,13 @@ class Entity {
304
320
  parameters = [parameters];
305
321
  }
306
322
  let concurrent = this._normalizeConcurrencyValue(config.concurrent)
307
- let concurrentOperations = utilities.batchItems(parameters, concurrent);
323
+ let concurrentOperations = u.batchItems(parameters, concurrent);
308
324
 
309
325
  let resultsAll = [];
310
326
  let unprocessedAll = [];
311
327
  for (let operation of concurrentOperations) {
312
328
  await Promise.all(operation.map(async params => {
313
- let response = await this._exec(MethodTypes.batchGet, params);
329
+ let response = await this._exec(MethodTypes.batchGet, params, config);
314
330
  if (validations.isFunction(config.parse)) {
315
331
  resultsAll.push(await config.parse(config, response));
316
332
  return;
@@ -340,7 +356,7 @@ class Entity {
340
356
  let limit = max === undefined
341
357
  ? parameters.Limit
342
358
  : max - count;
343
- let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit});
359
+ let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit}, config);
344
360
 
345
361
  ExclusiveStartKey = response.LastEvaluatedKey;
346
362
 
@@ -375,7 +391,7 @@ class Entity {
375
391
  }
376
392
 
377
393
  async executeOperation(method, parameters, config) {
378
- let response = await this._exec(method, parameters);
394
+ let response = await this._exec(method, parameters, config);
379
395
  if (validations.isFunction(config.parse)) {
380
396
  return config.parse(config, response);
381
397
  }
@@ -763,13 +779,14 @@ class Entity {
763
779
  _isPagination: false,
764
780
  _isCollectionQuery: false,
765
781
  pages: undefined,
782
+ listeners: [],
766
783
  };
767
784
 
768
785
  config = options.reduce((config, option) => {
769
786
  if (typeof option.response === 'string' && option.response.length) {
770
787
  const format = ReturnValues[option.response];
771
788
  if (format === undefined) {
772
- 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))}.`);
773
790
  }
774
791
  config.response = format;
775
792
  config.params.ReturnValues = FormatToReturnValues[format];
@@ -826,7 +843,7 @@ class Entity {
826
843
  if (typeof Pager[option.pager] === "string") {
827
844
  config.pager = option.pager;
828
845
  } else {
829
- 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))}.`);
830
847
  }
831
848
  }
832
849
 
@@ -834,7 +851,7 @@ class Entity {
834
851
  if (typeof UnprocessedTypes[option.unprocessed] === "string") {
835
852
  config.unproessed = UnprocessedTypes[option.unprocessed];
836
853
  } else {
837
- 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))}.`);
838
855
  }
839
856
  }
840
857
 
@@ -842,6 +859,20 @@ class Entity {
842
859
  config.ignoreOwnership = option.ignoreOwnership;
843
860
  }
844
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
+
845
876
  config.page = Object.assign({}, config.page, option.page);
846
877
  config.params = Object.assign({}, config.params, option.params);
847
878
  return config;
@@ -866,29 +897,18 @@ class Entity {
866
897
  return {parameters, config};
867
898
  }
868
899
 
869
- _makeItemDoesntExistConditions(index) {
870
- let hasSortKey = this.model.lookup.indexHasSortKeys[index];
871
- let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index];
900
+ _getPrimaryIndexFieldNames() {
901
+ let hasSortKey = this.model.lookup.indexHasSortKeys[TableIndex];
902
+ let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
872
903
  let pkField = this.model.indexes[accessPattern].pk.field;
873
- let filter = [`attribute_not_exists(${pkField})`];
904
+ let skField;
874
905
  if (hasSortKey) {
875
- let skField = this.model.indexes[accessPattern].sk.field;
876
- filter.push(`attribute_not_exists(${skField})`);
906
+ skField = this.model.indexes[accessPattern].sk.field;
877
907
  }
878
- return filter.join(" AND ");
879
- }
880
-
881
- _makeItemExistsConditions(index) {
882
- let hasSortKey = this.model.lookup.indexHasSortKeys[index];
883
- let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index];
884
- let pkField = this.model.indexes[accessPattern].pk.field;
885
-
886
- let filter = [`attribute_exists(${pkField})`];
887
- if (hasSortKey) {
888
- let skField = this.model.indexes[accessPattern].sk.field;
889
- filter.push(`attribute_exists(${skField})`);
908
+ return {
909
+ pk: pkField,
910
+ sk: skField
890
911
  }
891
- return filter.join(" AND ");
892
912
  }
893
913
 
894
914
  _applyParameterExpressionTypes(params, filter) {
@@ -956,7 +976,7 @@ class Entity {
956
976
  records.push(Key);
957
977
  }
958
978
  }
959
- let batches = utilities.batchItems(records, MaxBatchItems.batchGet);
979
+ let batches = u.batchItems(records, MaxBatchItems.batchGet);
960
980
  return batches.map(batch => {
961
981
  return {
962
982
  RequestItems: {
@@ -989,7 +1009,7 @@ class Entity {
989
1009
  throw new Error("Invalid method type");
990
1010
  }
991
1011
  }
992
- let batches = utilities.batchItems(records, MaxBatchItems.batchWrite);
1012
+ let batches = u.batchItems(records, MaxBatchItems.batchWrite);
993
1013
  return batches.map(batch => {
994
1014
  return {
995
1015
  RequestItems: {
@@ -1243,7 +1263,7 @@ class Entity {
1243
1263
  let props = Object.keys(item);
1244
1264
  let missing = require.filter((prop) => !props.includes(prop));
1245
1265
  if (!missing) {
1246
- 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)}`);
1247
1267
  }
1248
1268
  }
1249
1269
 
@@ -1252,7 +1272,7 @@ class Entity {
1252
1272
  throw new Error(`Invalid attribute ${prop}`);
1253
1273
  }
1254
1274
  if (restrict.length && !restrict.includes(prop)) {
1255
- 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)}`);
1256
1276
  }
1257
1277
  if (prop === undefined || skip.includes(prop)) {
1258
1278
  continue;
@@ -1415,7 +1435,7 @@ class Entity {
1415
1435
  _makeComparisonQueryParams(index = TableIndex, comparison = "", filter = {}, pk = {}, sk = {}) {
1416
1436
  let operator = Comparisons[comparison];
1417
1437
  if (!operator) {
1418
- 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))}`);
1419
1439
  }
1420
1440
  let keyExpressions = this._queryKeyExpressionAttributeBuilder(
1421
1441
  index,
@@ -1452,7 +1472,7 @@ class Entity {
1452
1472
  let incompleteAccessPatterns = incomplete.map(({index}) => this.model.translations.indexes.fromIndexToAccessPattern[index]);
1453
1473
  let missingFacets = incomplete.reduce((result, { missing }) => [...result, ...missing], []);
1454
1474
  throw new e.ElectroError(e.ErrorCodes.IncompleteCompositeAttributes,
1455
- `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))} `,
1456
1476
  );
1457
1477
  }
1458
1478
  return complete;
@@ -1671,7 +1691,7 @@ class Entity {
1671
1691
  _expectFacets(obj = {}, properties = [], type = "key composite attributes") {
1672
1692
  let [incompletePk, missing, matching] = this._expectProperties(obj, properties);
1673
1693
  if (incompletePk) {
1674
- 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)}`);
1675
1695
  } else {
1676
1696
  return matching;
1677
1697
  }
@@ -1744,10 +1764,10 @@ class Entity {
1744
1764
 
1745
1765
  // If keys arent custom, set the prefixes
1746
1766
  if (!keys.pk.isCustom) {
1747
- keys.pk.prefix = utilities.formatKeyCasing(pk, tableIndex.pk.casing);
1767
+ keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
1748
1768
  }
1749
1769
  if (!keys.sk.isCustom) {
1750
- keys.sk.prefix = utilities.formatKeyCasing(sk, tableIndex.sk.casing);
1770
+ keys.sk.prefix = u.formatKeyCasing(sk, tableIndex.sk.casing);
1751
1771
  }
1752
1772
 
1753
1773
  return keys;
@@ -1758,7 +1778,7 @@ class Entity {
1758
1778
  ? this.model.indexes[accessPattern].sk.casing
1759
1779
  : undefined;
1760
1780
 
1761
- return utilities.formatKeyCasing(key, casing);
1781
+ return u.formatKeyCasing(key, casing);
1762
1782
  }
1763
1783
 
1764
1784
  _validateIndex(index) {
@@ -1882,7 +1902,7 @@ class Entity {
1882
1902
  key = `${key}${supplied[name]}`;
1883
1903
  }
1884
1904
 
1885
- return utilities.formatKeyCasing(key, casing);
1905
+ return u.formatKeyCasing(key, casing);
1886
1906
  }
1887
1907
 
1888
1908
  _findBestIndexKeyMatch(attributes = {}) {
@@ -2162,7 +2182,7 @@ class Entity {
2162
2182
  let indexName = index.index || TableIndex;
2163
2183
  if (seenIndexes[indexName] !== undefined) {
2164
2184
  if (indexName === TableIndex) {
2165
- 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.`);
2166
2186
  } else {
2167
2187
  throw new e.ElectroError(e.ErrorCodes.DuplicateIndexes, `Duplicate index defined in model found in Access Pattern '${accessPattern}': '${indexName}'`);
2168
2188
  }
@@ -2171,7 +2191,7 @@ class Entity {
2171
2191
  let hasSk = !!index.sk;
2172
2192
  let inCollection = !!index.collection;
2173
2193
  if (!hasSk && inCollection) {
2174
- 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.`);
2175
2195
  }
2176
2196
  let collection = index.collection || "";
2177
2197
  let customFacets = {
@@ -2242,7 +2262,7 @@ class Entity {
2242
2262
  if (Array.isArray(sk.facets)) {
2243
2263
  let duplicates = pk.facets.filter(facet => sk.facets.includes(facet));
2244
2264
  if (duplicates.length !== 0) {
2245
- 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.`);
2246
2266
  }
2247
2267
  }
2248
2268
 
@@ -2345,13 +2365,13 @@ class Entity {
2345
2365
 
2346
2366
  let pkTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedPKAttributes, index.pk.composite);
2347
2367
  if (!pkTemplateIsCompatible) {
2348
- 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)}`);
2349
2369
  }
2350
2370
 
2351
2371
  if (index.sk !== undefined && Array.isArray(index.sk.composite) && typeof index.sk.template === "string") {
2352
2372
  let skTemplateIsCompatible = this._compositeTemplateAreCompatible(parsedSKAttributes, index.sk.composite);
2353
2373
  if (!skTemplateIsCompatible) {
2354
- 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)}`);
2355
2375
  }
2356
2376
  }
2357
2377
  }
@@ -2379,7 +2399,7 @@ class Entity {
2379
2399
 
2380
2400
  for (let [name, fn] of Object.entries(filters)) {
2381
2401
  if (invalidFilterNames.includes(name)) {
2382
- 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)}`);
2383
2403
  } else {
2384
2404
  normalized[name] = fn;
2385
2405
  }
@@ -2498,7 +2518,7 @@ class Entity {
2498
2518
  _parseModel(model, config = {}) {
2499
2519
  /** start beta/v1 condition **/
2500
2520
  const {client} = config;
2501
- let modelVersion = utilities.getModelVersion(model);
2521
+ let modelVersion = u.getModelVersion(model);
2502
2522
  let service, entity, version, table, name;
2503
2523
  switch(modelVersion) {
2504
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/operations.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const {AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes} = require("./types");
2
2
  const e = require("./errors");
3
- const v = require("./util");
3
+ const u = require("./util");
4
4
 
5
5
  const deleteOperations = {
6
6
  canNest: false,
@@ -21,6 +21,13 @@ const deleteOperations = {
21
21
  };
22
22
 
23
23
  const UpdateOperations = {
24
+ ifNotExists: {
25
+ template: function if_not_exists(options, attr, path, value) {
26
+ const operation = ItemOperations.set;
27
+ const expression = `${path} = if_not_exists(${path}, ${value})`;
28
+ return {operation, expression};
29
+ }
30
+ },
24
31
  name: {
25
32
  canNest: true,
26
33
  template: function name(options, attr, path) {
@@ -325,7 +332,7 @@ class AttributeOperationProxy {
325
332
  fromObject(operation, record) {
326
333
  for (let path of Object.keys(record)) {
327
334
  const value = record[path];
328
- const parts = v.parseJSONPath(path);
335
+ const parts = u.parseJSONPath(path);
329
336
  let attribute = this.attributes;
330
337
  for (let part of parts) {
331
338
  attribute = attribute[part];
@@ -342,7 +349,7 @@ class AttributeOperationProxy {
342
349
 
343
350
  fromArray(operation, paths) {
344
351
  for (let path of paths) {
345
- const parts = v.parseJSONPath(path);
352
+ const parts = u.parseJSONPath(path);
346
353
  let attribute = this.attributes;
347
354
  for (let part of parts) {
348
355
  attribute = attribute[part];
@@ -453,4 +460,9 @@ class AttributeOperationProxy {
453
460
  }
454
461
  }
455
462
 
456
- module.exports = {UpdateOperations, FilterOperations, ExpressionState, AttributeOperationProxy};
463
+ const FilterOperationNames = Object.keys(FilterOperations).reduce((ops, name) => {
464
+ ops[name] = name;
465
+ return ops;
466
+ }, {});
467
+
468
+ module.exports = {UpdateOperations, FilterOperations, FilterOperationNames, ExpressionState, AttributeOperationProxy};
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") {
package/src/where.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const {MethodTypes, ExpressionTypes, BuilderTypes} = require("./types");
2
- const {AttributeOperationProxy, ExpressionState} = require("./operations");
2
+ const {AttributeOperationProxy, ExpressionState, FilterOperations} = require("./operations");
3
3
  const e = require("./errors");
4
4
 
5
5
  class FilterExpression extends ExpressionState {
@@ -43,6 +43,22 @@ class FilterExpression extends ExpressionState {
43
43
  this.expression = expression;
44
44
  }
45
45
 
46
+ // applies operations without verifying them against known attributes. Used internally for key conditions.
47
+ unsafeSet(operation, name, ...values) {
48
+ const {template} = FilterOperations[operation] || {};
49
+ if (template === undefined) {
50
+ throw new Error(`Invalid operation: "${operation}". Please report`);
51
+ }
52
+ const names = this.setName({}, name, name);
53
+ if (values.length) {
54
+ for (const value of values) {
55
+ this.setValue(name, value);
56
+ }
57
+ }
58
+ const condition = template({}, name.expression, names.prop, ...values);
59
+ this.add(condition);
60
+ }
61
+
46
62
  build() {
47
63
  return this.expression;
48
64
  }