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 +5 -1
- package/README.md +258 -9
- package/index.d.ts +60 -6
- package/package.json +1 -1
- package/src/clauses.js +3 -2
- package/src/entity.js +74 -31
- package/src/errors.js +7 -1
- package/src/events.js +67 -0
- package/src/service.js +6 -0
- package/src/types.js +7 -1
- package/src/util.js +2 -1
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
|
-
|
|
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()
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
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 (
|
|
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 (
|
|
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
|
|
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: ${
|
|
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,
|
|
256
|
-
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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: ${
|
|
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: ${
|
|
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 ${
|
|
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 ${
|
|
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: ${
|
|
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 =
|
|
1767
|
+
keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
|
|
1725
1768
|
}
|
|
1726
1769
|
if (!keys.sk.isCustom) {
|
|
1727
|
-
keys.sk.prefix =
|
|
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
|
|
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
|
|
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}': '${
|
|
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}': '${
|
|
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): ${
|
|
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 "${
|
|
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 "${
|
|
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 ${
|
|
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 =
|
|
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
|
};
|