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 +13 -1
- package/README.md +261 -11
- package/index.d.ts +61 -6
- package/package.json +1 -1
- package/src/clauses.js +23 -4
- package/src/entity.js +85 -65
- package/src/errors.js +7 -1
- package/src/events.js +67 -0
- package/src/operations.js +16 -4
- package/src/service.js +6 -0
- package/src/types.js +7 -1
- package/src/util.js +2 -1
- package/src/where.js +17 -1
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
|
[](https://coveralls.io/github/tywalch/electrodb?branch=master&kill_cache=please)
|
|
3
3
|
[](https://www.npmjs.com/package/electrodb)
|
|
4
4
|
 [](https://travis-ci.org/tywalch/electrodb)
|
|
5
|
-
[](https://runkit.com/tywalch/
|
|
5
|
+
[](https://runkit.com/tywalch/electrodb-building-queries)
|
|
6
6
|
|
|
7
7
|

|
|
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
|
-
|
|
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,
|
|
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()
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
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 (
|
|
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 (
|
|
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
|
|
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
|
}
|
|
@@ -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,
|
|
268
|
-
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
870
|
-
let hasSortKey = this.model.lookup.indexHasSortKeys[
|
|
871
|
-
let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[
|
|
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
|
|
904
|
+
let skField;
|
|
874
905
|
if (hasSortKey) {
|
|
875
|
-
|
|
876
|
-
filter.push(`attribute_not_exists(${skField})`);
|
|
906
|
+
skField = this.model.indexes[accessPattern].sk.field;
|
|
877
907
|
}
|
|
878
|
-
return
|
|
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 =
|
|
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 =
|
|
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: ${
|
|
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: ${
|
|
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 ${
|
|
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 ${
|
|
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: ${
|
|
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 =
|
|
1767
|
+
keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
|
|
1748
1768
|
}
|
|
1749
1769
|
if (!keys.sk.isCustom) {
|
|
1750
|
-
keys.sk.prefix =
|
|
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
|
|
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
|
|
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}': '${
|
|
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}': '${
|
|
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): ${
|
|
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 "${
|
|
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 "${
|
|
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 ${
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
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
|
}
|