electrodb 1.5.0 → 1.6.3
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 +30 -6
- package/README.md +114 -58
- package/index.d.ts +40 -0
- package/package.json +6 -3
- package/src/clauses.js +20 -2
- package/src/entity.js +105 -77
- package/src/errors.js +92 -8
- package/src/operations.js +16 -4
- package/src/schema.js +101 -50
- package/src/where.js +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -98,28 +98,52 @@ All notable changes to this project will be documented in this file. Breaking ch
|
|
|
98
98
|
### Patched
|
|
99
99
|
- Updates did not include composite attributes involved in primary index. Though these values cannot be changed, they should be `set` on update method calls in case the update results in an item insert. [[read more]](./README.md#updates-to-composite-attributes)
|
|
100
100
|
|
|
101
|
-
## [1.4.5]
|
|
101
|
+
## [1.4.5] - 2021-10-17
|
|
102
102
|
### Fixed
|
|
103
103
|
- Improved .npmignore to remove playground oriented files, and created official directory to keep playground in sync with library changes.
|
|
104
104
|
|
|
105
|
-
## [1.4.6]
|
|
105
|
+
## [1.4.6] - 2021-10-20
|
|
106
106
|
### Added, Fixed
|
|
107
107
|
- Adding Entity identifiers to all update operations. When primary index composite attributes were added in 1.4.4, entities were written properly but did not include the identifiers. This resulted in entities being written but not being readable without the query option `ignoreOwnership` being used.
|
|
108
108
|
|
|
109
|
-
## [1.4.7]
|
|
109
|
+
## [1.4.7] - 2021-10-20
|
|
110
110
|
### Changed
|
|
111
111
|
- Using `add()` update mutation now resolves to `ADD #prop :prop` update expression instead of a `SET #prop = #prop + :prop`
|
|
112
112
|
|
|
113
113
|
### Fixed
|
|
114
114
|
- Fixed param naming conflict during updates, when map attribute shares a name with another (separate) attribute.
|
|
115
115
|
|
|
116
|
-
## [1.4.8]
|
|
116
|
+
## [1.4.8] - 2021-11-01
|
|
117
117
|
### Fixed
|
|
118
118
|
- Addressed issue#90 to flip batchGet's response tuple type definition.
|
|
119
119
|
|
|
120
|
-
## [1.5.0]
|
|
120
|
+
## [1.5.0] - 2021-11-07
|
|
121
121
|
### Changed
|
|
122
122
|
- Queries will now fully paginate all responses. Prior to this change, ElectroDB would only return items from a single ElectroDB query result. Now ElectroDB will paginate through all query results. This will impact both uses of entity queries and service collections. [[read more](./README.md#query-method)]
|
|
123
123
|
- The query option `limit` has an extended meaning with the change to automatically paginate records on query. The option `limit` now represents 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. [[read more](./README.md#query-options)]
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
- A new query option `pages` has been added to coincide with the change to automatically paginate all records when queried. The `pages` option sets a max number of pagination iterations ElectroDB will perform on a query. When this option is paired with `limit`, ElectroDB will respect the first condition reached. [[read more](./README.md#query-options)]
|
|
127
|
+
|
|
128
|
+
## [1.6.0] - 2021-11-21
|
|
129
|
+
### Added
|
|
130
|
+
- Exporting TypeScript interfaces for `ElectroError` and `ElectroValidationError`
|
|
131
|
+
- Errors thrown within an attribute's validate callback are now wrapped and accessible after being thrown. Prior to this change, only the `message` of the error thrown by a validation function was persisted back through to the user, now the error itself is also accessible. Reference the exported interface typedef for `ElectroValidationError` [here](./index.d.ts) to see the new properties available on a thrown validation error.
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
- As a byproduct of enhancing validation errors, the format of message text on a validation error has changed. This could be breaking if your app had a hardcoded dependency on the exact text of a thrown validation error.
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
- For Set attributes, the callback functions `get`, `set`, and `validate` are now consistently given an Array of values. These functions would sometimes (incorrectly) be called with a DynamoDB DocClient Set.
|
|
138
|
+
|
|
139
|
+
## [1.6.1] - 2021-12-05
|
|
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.
|
|
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
|
|
124
148
|
### Added
|
|
125
|
-
-
|
|
149
|
+
- Add `data` update operation `ifNotExists` to allow for use of the UpdateExpression function "if_not_exists()".
|
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.
|
|
@@ -107,11 +107,11 @@ tasks
|
|
|
107
107
|
* [TypeScript Support](#typescript-support)
|
|
108
108
|
+ [TypeScript Services](#typescript-services)
|
|
109
109
|
* [Join](#join)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
- [Independent Models](#independent-models)
|
|
111
|
+
- [Joining Entity instances to a Service](#joining-entity-instances-to-a-service)
|
|
112
|
+
- [Joining models to a Service](#joining-models-to-a-service)
|
|
113
|
+
- [Joining Entities or Models with an alias](#joining-entities-or-models-with-an-alias)
|
|
114
|
+
- [Joining Entities at Service construction for TypeScript](#joining-entities-at-service-construction-for-typescript)
|
|
115
115
|
* [Model](#model)
|
|
116
116
|
+ [Model Properties](#model-properties)
|
|
117
117
|
+ [Service Options](#service-options)
|
|
@@ -125,7 +125,7 @@ tasks
|
|
|
125
125
|
- [Set Attributes](#set-attributes)
|
|
126
126
|
- [Attribute Getters and Setters](#attribute-getters-and-setters)
|
|
127
127
|
- [Attribute Watching](#attribute-watching)
|
|
128
|
-
* [Attribute Watching: Watch All](#attribute-watching
|
|
128
|
+
* [Attribute Watching: Watch All](#attribute-watching--watch-all)
|
|
129
129
|
* [Attribute Watching Examples](#attribute-watching-examples)
|
|
130
130
|
- [Calculated Attributes](#calculated-attributes)
|
|
131
131
|
- [Virtual Attributes](#virtual-attributes)
|
|
@@ -147,7 +147,7 @@ tasks
|
|
|
147
147
|
+ [Collection Queries vs Entity Queries](#collection-queries-vs-entity-queries)
|
|
148
148
|
+ [Collection Response Structure](#collection-response-structure)
|
|
149
149
|
* [Sub-Collections](#sub-collections)
|
|
150
|
-
|
|
150
|
+
- [Sub-Collection Entities](#sub-collection-entities)
|
|
151
151
|
* [Index and Collection Naming Conventions](#index-and-collection-naming-conventions)
|
|
152
152
|
+ [Index Naming Conventions](#index-naming-conventions)
|
|
153
153
|
* [Collection Naming Conventions](#collection-naming-conventions)
|
|
@@ -163,11 +163,11 @@ tasks
|
|
|
163
163
|
+ [Multiple Where Clauses](#multiple-where-clauses)
|
|
164
164
|
* [Parse](#parse)
|
|
165
165
|
- [Building Queries](#building-queries)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
+ [Using composite attributes to make hierarchical keys](#using-composite-attributes-to-make-hierarchical-keys)
|
|
167
|
+
- [Shopping Mall Stores](#shopping-mall-stores)
|
|
168
|
+
+ [Query App Records](#query-app-records)
|
|
169
|
+
- [Partition Key Composite Attributes](#partition-key-composite-attributes)
|
|
170
|
+
+ [Sort Key Operations](#sort-key-operations)
|
|
171
171
|
* [Query Chains](#query-chains)
|
|
172
172
|
+ [Query Method](#query-method)
|
|
173
173
|
+ [Get Method](#get-method)
|
|
@@ -178,14 +178,14 @@ tasks
|
|
|
178
178
|
+ [Batch Write Put Records](#batch-write-put-records)
|
|
179
179
|
+ [Update Record](#update-record)
|
|
180
180
|
- [Updates to Composite Attributes](#updates-to-composite-attributes)
|
|
181
|
-
- [Update Method: Set](#update-method
|
|
182
|
-
- [Update Method: Remove](#update-method
|
|
183
|
-
- [Update Method: Add](#update-method
|
|
184
|
-
- [Update Method: Subtract](#update-method
|
|
185
|
-
- [Update Method: Append](#update-method
|
|
186
|
-
- [Update Method: Delete](#update-method
|
|
187
|
-
- [Update Method: Data](#update-method
|
|
188
|
-
+ [Update Method: Complex Data Types](#update-method
|
|
181
|
+
- [Update Method: Set](#update-method--set)
|
|
182
|
+
- [Update Method: Remove](#update-method--remove)
|
|
183
|
+
- [Update Method: Add](#update-method--add)
|
|
184
|
+
- [Update Method: Subtract](#update-method--subtract)
|
|
185
|
+
- [Update Method: Append](#update-method--append)
|
|
186
|
+
- [Update Method: Delete](#update-method--delete)
|
|
187
|
+
- [Update Method: Data](#update-method--data)
|
|
188
|
+
+ [Update Method: Complex Data Types](#update-method--complex-data-types)
|
|
189
189
|
+ [Scan Records](#scan-records)
|
|
190
190
|
+ [Remove Method](#remove-method)
|
|
191
191
|
+ [Patch Record](#patch-record)
|
|
@@ -205,30 +205,33 @@ tasks
|
|
|
205
205
|
* [Pagination Example](#pagination-example)
|
|
206
206
|
* [Query Examples](#query-examples)
|
|
207
207
|
* [Query Options](#query-options)
|
|
208
|
-
- [Errors:](#errors)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
208
|
+
- [Errors:](#errors-)
|
|
209
|
+
+ [No Client Defined On Model](#no-client-defined-on-model)
|
|
210
|
+
+ [Invalid Identifier](#invalid-identifier)
|
|
211
|
+
+ [Invalid Key Composite Attribute Template](#invalid-key-composite-attribute-template)
|
|
212
|
+
+ [Duplicate Indexes](#duplicate-indexes)
|
|
213
|
+
+ [Collection Without An SK](#collection-without-an-sk)
|
|
214
|
+
+ [Duplicate Collections](#duplicate-collections)
|
|
215
|
+
+ [Missing Primary Index](#missing-primary-index)
|
|
216
|
+
+ [Invalid Attribute Definition](#invalid-attribute-definition)
|
|
217
|
+
+ [Invalid Model](#invalid-model)
|
|
218
|
+
+ [Invalid Options](#invalid-options)
|
|
219
|
+
+ [Duplicate Index Fields](#duplicate-index-fields)
|
|
220
|
+
+ [Duplicate Index Composite Attributes](#duplicate-index-composite-attributes)
|
|
221
|
+
+ [Incompatible Key Composite Attribute Template](#incompatible-key-composite-attribute-template)
|
|
222
|
+
+ [Invalid Index With Attribute Name](#invalid-index-with-attribute-name)
|
|
223
|
+
+ [Invalid Collection on Index With Attribute Field Names](#invalid-collection-on-index-with-attribute-field-names)
|
|
224
|
+
+ [Missing Composite Attributes](#missing-composite-attributes)
|
|
225
|
+
+ [Missing Table](#missing-table)
|
|
226
|
+
+ [Invalid Concurrency Option](#invalid-concurrency-option)
|
|
227
|
+
+ [Invalid Pages Option](#invalid-pages-option)
|
|
228
|
+
+ [Invalid Limit Option](#invalid-limit-option)
|
|
229
|
+
+ [Invalid Attribute](#invalid-attribute)
|
|
230
|
+
+ [AWS Error](#aws-error)
|
|
231
|
+
+ [Unknown Errors](#unknown-errors)
|
|
232
|
+
+ [Invalid Last Evaluated Key](#invalid-last-evaluated-key)
|
|
233
|
+
+ [No Owner For Pager](#no-owner-for-pager)
|
|
234
|
+
+ [Pager Not Unique](#pager-not-unique)
|
|
232
235
|
- [Examples](#examples)
|
|
233
236
|
* [Employee App](#employee-app)
|
|
234
237
|
+ [Employee App Requirements](#employee-app-requirements)
|
|
@@ -566,7 +569,7 @@ const TasksModel = {
|
|
|
566
569
|
attributes: {
|
|
567
570
|
task: {
|
|
568
571
|
type: "string",
|
|
569
|
-
default: () =>
|
|
572
|
+
default: () => uuid(),
|
|
570
573
|
},
|
|
571
574
|
project: {
|
|
572
575
|
type: "string",
|
|
@@ -816,11 +819,11 @@ myAttr: {
|
|
|
816
819
|
watch: ["otherAttr"],
|
|
817
820
|
set: (myAttr, {otherAttr}) => {
|
|
818
821
|
// Whenever "myAttr" or "otherAttr" are updated from an `update` or `patch` operation, this callback will be fired.
|
|
819
|
-
// Note: myAttr or otherAttr could be
|
|
822
|
+
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback
|
|
820
823
|
},
|
|
821
824
|
get: (myAttr, {otherAttr}) => {
|
|
822
825
|
// Whenever "myAttr" or "otherAttr" are retrieved from a `query` or `get` operation, this callback will be fired.
|
|
823
|
-
// Note: myAttr or otherAttr could be
|
|
826
|
+
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback.
|
|
824
827
|
}
|
|
825
828
|
}
|
|
826
829
|
```
|
|
@@ -834,11 +837,11 @@ myAttr: {
|
|
|
834
837
|
watch: "*", // "watch all"
|
|
835
838
|
set: (myAttr, allAttributes) => {
|
|
836
839
|
// Whenever an `update` or `patch` operation is performed, this callback will be fired.
|
|
837
|
-
// Note: myAttr or the attributes under `allAttributes` could be
|
|
840
|
+
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
|
|
838
841
|
},
|
|
839
842
|
get: (myAttr, allAttributes) => {
|
|
840
843
|
// Whenever a `query` or `get` operation is performed, this callback will be fired.
|
|
841
|
-
// Note: myAttr or the attributes under `allAttributes` could be
|
|
844
|
+
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
|
|
842
845
|
}
|
|
843
846
|
}
|
|
844
847
|
```
|
|
@@ -1418,7 +1421,7 @@ As described in the above two sections ([Composite Attributes](#composite-attrib
|
|
|
1418
1421
|
|
|
1419
1422
|
It may be the case that an index field is also an attribute. For example, if a table was created with a Primary Index partition key of `accountId`, and that same field is used to store the `accountId` value used by the application. The following are a few examples of how to model that schema with ElectroDB:
|
|
1420
1423
|
|
|
1421
|
-
> _NOTE: If you have the unique opportunity to use ElectroDB with a new project, it is strongly recommended to use
|
|
1424
|
+
> _NOTE: If you have the unique opportunity to use ElectroDB with a new project, it is strongly recommended to use generically named index fields that are separate from your business attributes._
|
|
1422
1425
|
|
|
1423
1426
|
**Using `composite`**
|
|
1424
1427
|
|
|
@@ -1759,7 +1762,7 @@ let results = await TaskApp.collections
|
|
|
1759
1762
|
|
|
1760
1763
|
{
|
|
1761
1764
|
tasks: [...], // tasks for employeeId "JExotic"
|
|
1762
|
-
employees: [...] // employee record(s) with employeeId "
|
|
1765
|
+
employees: [...] // employee record(s) with employeeId "JExotic"
|
|
1763
1766
|
}
|
|
1764
1767
|
```
|
|
1765
1768
|
|
|
@@ -1774,7 +1777,7 @@ The following is an example of functionally identical collections, implemented a
|
|
|
1774
1777
|
**As a string (collection):**
|
|
1775
1778
|
```typescript
|
|
1776
1779
|
{
|
|
1777
|
-
|
|
1780
|
+
collection: "assignments"
|
|
1778
1781
|
pk: {
|
|
1779
1782
|
field: "pk",
|
|
1780
1783
|
composite: ["employeeId"]
|
|
@@ -1789,7 +1792,7 @@ The following is an example of functionally identical collections, implemented a
|
|
|
1789
1792
|
**As a string array (sub-collections):**
|
|
1790
1793
|
```typescript
|
|
1791
1794
|
{
|
|
1792
|
-
|
|
1795
|
+
collection: ["assignments"]
|
|
1793
1796
|
pk: {
|
|
1794
1797
|
field: "pk",
|
|
1795
1798
|
composite: ["employeeId"]
|
|
@@ -3459,7 +3462,8 @@ operation | example | result
|
|
|
3459
3462
|
`delete` | `delete(tenant, name)` | `#tenant :tenant1` | Remove item from existing `set` attribute
|
|
3460
3463
|
`del` | `del(tenant, name)` | `#tenant :tenant1` | Alias for `delete` operation
|
|
3461
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
|
|
3462
|
-
`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
|
|
3463
3467
|
|
|
3464
3468
|
```javascript
|
|
3465
3469
|
await StoreLocations
|
|
@@ -3717,6 +3721,8 @@ DynamoDB offers three methods to query records: `get`, `query`, and `scan`. In *
|
|
|
3717
3721
|
|
|
3718
3722
|
> _NOTE: The Find method is similar to the Match method with one exception: The attributes you supply directly to the `.find()` method will only be used to identify and fulfill your index access patterns. Any values supplied that do not contribute to a composite key will not be applied as query filters. Furthermore, if the values you provide do not resolve to an index access pattern, then a table scan will be performed. Use the `where()` chain method to further filter beyond keys, or use [Match](#match-records) for the convenience of automatic filtering based on the values given directly to that method._
|
|
3719
3723
|
|
|
3724
|
+
The Find method is useful when the index chosen does not matter or is not known. If your secondary indexes do not contain all attributes then this method might not be right for you. The mechanism that picks the best index for a given payload is subject to improvement and change without triggering a breaking change release version.
|
|
3725
|
+
|
|
3720
3726
|
```javascript
|
|
3721
3727
|
await StoreLocations.find({
|
|
3722
3728
|
mallId: "EastPointe",
|
|
@@ -3747,8 +3753,11 @@ await StoreLocations.find({
|
|
|
3747
3753
|
|
|
3748
3754
|
Match is a convenience method based off of ElectroDB's [find](#find-records) method. Similar to Find, Match does not require you to provide keys, but under the covers it will leverage the attributes provided to choose the best index to query on.
|
|
3749
3755
|
|
|
3756
|
+
> _NOTE: The Math method is useful when the index chosen does not matter or is not known. If your secondary indexes do not contain all attributes then this method might not be right for you. The mechanism that picks the best index for a given payload is subject to improvement and change without triggering a breaking change release version.
|
|
3757
|
+
|
|
3750
3758
|
Match differs from [Find](#find-records) in that it will also include all supplied values into a query filter.
|
|
3751
3759
|
|
|
3760
|
+
|
|
3752
3761
|
```javascript
|
|
3753
3762
|
await StoreLocations.find({
|
|
3754
3763
|
mallId: "EastPointe",
|
|
@@ -4225,7 +4234,7 @@ await StoreLocations.query.leases({storeId}).gte({leaseEndDate: "2020-03"}).go()
|
|
|
4225
4234
|
// Lease Agreements by StoreId before 2021
|
|
4226
4235
|
await StoreLocations.query.leases({storeId}).lt({leaseEndDate: "2021-01"}).go()
|
|
4227
4236
|
|
|
4228
|
-
// Lease Agreements by StoreId before
|
|
4237
|
+
// Lease Agreements by StoreId before February 2021
|
|
4229
4238
|
await StoreLocations.query.leases({storeId}).lte({leaseEndDate: "2021-02"}).go()
|
|
4230
4239
|
|
|
4231
4240
|
// Lease Agreements by StoreId between 2010 and 2020
|
|
@@ -4572,7 +4581,54 @@ When performing a query [Query](#building-queries) you can pass a [Query Options
|
|
|
4572
4581
|
*What to do about it:*
|
|
4573
4582
|
Expect this error only if you're providing a `limit` option. Double-check the value you are providing is the value you expect to be passing, and that the value passes the tests listed above.
|
|
4574
4583
|
|
|
4575
|
-
###
|
|
4584
|
+
### Invalid Attribute
|
|
4585
|
+
*Code: 3001*
|
|
4586
|
+
|
|
4587
|
+
*Why this occurred:*
|
|
4588
|
+
The value received for a validation either failed type expectations (e.g. a "number" instead of a "string"), or the user provided "validate" callback on an attribute rejected a value.
|
|
4589
|
+
|
|
4590
|
+
*What to do about it:*
|
|
4591
|
+
Examine the error itself for more precise detail on why the failure occurred. The error object itself should have a property called "fields" which contains an array of every attribute that failed validation, and a reason for each. If the failure originated from a "validate" callback, the originally thrown error will be accessible via the `cause` property the corresponding element within the fields array.1
|
|
4592
|
+
|
|
4593
|
+
Below is the type definition for an ElectroValidationError:
|
|
4594
|
+
|
|
4595
|
+
```typescript
|
|
4596
|
+
ElectroValidationError<T extends Error = Error> extends ElectroError {
|
|
4597
|
+
readonly name: "ElectroValidationError"
|
|
4598
|
+
readonly code: number;
|
|
4599
|
+
readonly date: number;
|
|
4600
|
+
readonly isElectroError: boolean;
|
|
4601
|
+
ref: {
|
|
4602
|
+
readonly code: number;
|
|
4603
|
+
readonly section: string;
|
|
4604
|
+
readonly name: string;
|
|
4605
|
+
readonly sym: unique symbol;
|
|
4606
|
+
}
|
|
4607
|
+
readonly fields: ReadonlyArray<{
|
|
4608
|
+
/**
|
|
4609
|
+
* The json path to the attribute that had a validation error
|
|
4610
|
+
*/
|
|
4611
|
+
readonly field: string;
|
|
4612
|
+
|
|
4613
|
+
/**
|
|
4614
|
+
* A description of the validation error for that attribute
|
|
4615
|
+
*/
|
|
4616
|
+
readonly reason: string;
|
|
4617
|
+
|
|
4618
|
+
/**
|
|
4619
|
+
* Index of the value passed (present only in List attribute validation errors)
|
|
4620
|
+
*/
|
|
4621
|
+
readonly index: number | undefined;
|
|
4622
|
+
|
|
4623
|
+
/**
|
|
4624
|
+
* The error thrown from the attribute's validate callback (if applicable)
|
|
4625
|
+
*/
|
|
4626
|
+
readonly cause: T | undefined;
|
|
4627
|
+
}>
|
|
4628
|
+
}
|
|
4629
|
+
```
|
|
4630
|
+
|
|
4631
|
+
### AWS Error
|
|
4576
4632
|
*Code: 4001*
|
|
4577
4633
|
|
|
4578
4634
|
*Why this occurred:*
|
|
@@ -5497,4 +5553,4 @@ This change stems from the fact the `facets` is already a defined term in the Dy
|
|
|
5497
5553
|
1.0.0 brings back a `null` response from the `get()` method when a record could not be found. Prior to `1.0.0` ElectroDB returned an empty object.
|
|
5498
5554
|
|
|
5499
5555
|
# Coming Soon
|
|
5500
|
-
- Default query options defined on the `model` to give more general control of interactions with the Entity.
|
|
5556
|
+
- Default query options defined on the `model` to give more general control of interactions with the Entity.
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
declare const WhereSymbol: unique symbol;
|
|
2
2
|
declare const UpdateDataSymbol: unique symbol;
|
|
3
3
|
|
|
4
|
+
export interface ElectroError extends Error {
|
|
5
|
+
readonly name: 'ElectroError';
|
|
6
|
+
readonly code: number;
|
|
7
|
+
readonly date: number;
|
|
8
|
+
readonly isElectroError: boolean;
|
|
9
|
+
ref: {
|
|
10
|
+
readonly code: number;
|
|
11
|
+
readonly section: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly sym: unique symbol;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ElectroValidationErrorFieldReference<T extends Error = Error> {
|
|
18
|
+
/**
|
|
19
|
+
* The json path to the attribute that had a validation error
|
|
20
|
+
*/
|
|
21
|
+
readonly field: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A description of the validation error for that attribute
|
|
25
|
+
*/
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Index of the value passed (present only in List attribute validation errors)
|
|
30
|
+
*/
|
|
31
|
+
readonly index: number | undefined;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The error thrown from the attribute's validate callback (if applicable)
|
|
35
|
+
*/
|
|
36
|
+
readonly cause: T | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ElectroValidationError<T extends Error = Error> extends ElectroError {
|
|
40
|
+
readonly fields: ReadonlyArray<ElectroValidationErrorFieldReference<T>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
4
43
|
interface ReadOnlyAttribute {
|
|
5
44
|
readonly readOnly: true;
|
|
6
45
|
}
|
|
@@ -915,6 +954,7 @@ type DataUpdateOperations<A extends string, F extends A, C extends string, S ext
|
|
|
915
954
|
del: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: A extends DataUpdateAttributeSymbol<infer V> ? V extends Array<any> ? V : never : never ) => any;
|
|
916
955
|
value: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: DataUpdateAttributeValues<A>) => Required<DataUpdateAttributeValues<A>>;
|
|
917
956
|
name: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A) => any;
|
|
957
|
+
ifNotExists: <T, A extends DataUpdateAttributeSymbol<T>>(attr: A, value: DataUpdateAttributeValues<A>) => any;
|
|
918
958
|
};
|
|
919
959
|
|
|
920
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"]>> =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electrodb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/tywalch/electrodb#readme",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@istanbuljs/nyc-config-typescript": "^1.0.
|
|
28
|
+
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
29
29
|
"@types/chai": "^4.2.12",
|
|
30
30
|
"@types/mocha": "^8.0.3",
|
|
31
31
|
"@types/node": "^15.6.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"aws-sdk": "2.630.0",
|
|
34
34
|
"browserify": "^17.0.0",
|
|
35
35
|
"chai": "4.2.0",
|
|
36
|
-
"coveralls": "^3.1.
|
|
36
|
+
"coveralls": "^3.1.1",
|
|
37
37
|
"istanbul": "0.4.5",
|
|
38
38
|
"jest": "25.4.0",
|
|
39
39
|
"mocha": "7.1.1",
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"electrodb",
|
|
51
51
|
"dynamo",
|
|
52
52
|
"dynamodb",
|
|
53
|
+
"nosql",
|
|
54
|
+
"single table design",
|
|
55
|
+
"typescript",
|
|
53
56
|
"aws"
|
|
54
57
|
],
|
|
55
58
|
"tsd": {
|
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)
|
package/src/entity.js
CHANGED
|
@@ -198,11 +198,7 @@ class Entity {
|
|
|
198
198
|
|
|
199
199
|
create(attributes = {}) {
|
|
200
200
|
let index = TableIndex;
|
|
201
|
-
let options = {
|
|
202
|
-
params: {
|
|
203
|
-
ConditionExpression: this._makeItemDoesntExistConditions(index)
|
|
204
|
-
}
|
|
205
|
-
};
|
|
201
|
+
let options = {};
|
|
206
202
|
return this._makeChain(index, this._clausesWithFilters, clauses.index, options).create(attributes);
|
|
207
203
|
}
|
|
208
204
|
|
|
@@ -213,21 +209,13 @@ class Entity {
|
|
|
213
209
|
|
|
214
210
|
patch(facets = {}) {
|
|
215
211
|
let index = TableIndex;
|
|
216
|
-
let options = {
|
|
217
|
-
params: {
|
|
218
|
-
ConditionExpression: this._makeItemExistsConditions(index)
|
|
219
|
-
}
|
|
220
|
-
};
|
|
212
|
+
let options = {};
|
|
221
213
|
return this._makeChain(index, this._clausesWithFilters, clauses.index, options).patch(facets);
|
|
222
214
|
}
|
|
223
215
|
|
|
224
216
|
remove(facets = {}) {
|
|
225
217
|
let index = TableIndex;
|
|
226
|
-
let options = {
|
|
227
|
-
params: {
|
|
228
|
-
ConditionExpression: this._makeItemExistsConditions(index)
|
|
229
|
-
}
|
|
230
|
-
};
|
|
218
|
+
let options = {};
|
|
231
219
|
return this._makeChain(index, this._clausesWithFilters, clauses.index, options).remove(facets);
|
|
232
220
|
}
|
|
233
221
|
|
|
@@ -866,29 +854,18 @@ class Entity {
|
|
|
866
854
|
return {parameters, config};
|
|
867
855
|
}
|
|
868
856
|
|
|
869
|
-
|
|
870
|
-
let hasSortKey = this.model.lookup.indexHasSortKeys[
|
|
871
|
-
let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[
|
|
857
|
+
_getPrimaryIndexFieldNames() {
|
|
858
|
+
let hasSortKey = this.model.lookup.indexHasSortKeys[TableIndex];
|
|
859
|
+
let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
|
|
872
860
|
let pkField = this.model.indexes[accessPattern].pk.field;
|
|
873
|
-
let
|
|
861
|
+
let skField;
|
|
874
862
|
if (hasSortKey) {
|
|
875
|
-
|
|
876
|
-
filter.push(`attribute_not_exists(${skField})`);
|
|
863
|
+
skField = this.model.indexes[accessPattern].sk.field;
|
|
877
864
|
}
|
|
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})`);
|
|
865
|
+
return {
|
|
866
|
+
pk: pkField,
|
|
867
|
+
sk: skField
|
|
890
868
|
}
|
|
891
|
-
return filter.join(" AND ");
|
|
892
869
|
}
|
|
893
870
|
|
|
894
871
|
_applyParameterExpressionTypes(params, filter) {
|
|
@@ -1885,54 +1862,105 @@ class Entity {
|
|
|
1885
1862
|
return utilities.formatKeyCasing(key, casing);
|
|
1886
1863
|
}
|
|
1887
1864
|
|
|
1888
|
-
_findBestIndexKeyMatch(attributes) {
|
|
1889
|
-
|
|
1865
|
+
_findBestIndexKeyMatch(attributes = {}) {
|
|
1866
|
+
// an array of arrays, representing the order of pk and sk composites specified for each index, and then an
|
|
1867
|
+
// array with each access pattern occupying the same array index.
|
|
1890
1868
|
let facets = this.model.facets.bySlot;
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
for (let
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
let match = !!attributes[name];
|
|
1906
|
-
let matchNext = !!attributes[next];
|
|
1907
|
-
if (match) {
|
|
1908
|
-
keys[index] = keys[index] || [];
|
|
1909
|
-
keys[index].push({ name, type });
|
|
1910
|
-
currentMatches.push(slot);
|
|
1911
|
-
if (matchNext) {
|
|
1912
|
-
nextMatches.push(slot);
|
|
1913
|
-
}
|
|
1869
|
+
// a flat array containing the match results of each access pattern, in the same array index they occur within
|
|
1870
|
+
// bySlot above
|
|
1871
|
+
let matches = [];
|
|
1872
|
+
for (let f = 0; f < facets.length; f++) {
|
|
1873
|
+
const slots = facets[f] || [];
|
|
1874
|
+
for (let s = 0; s < slots.length; s++) {
|
|
1875
|
+
const accessPatternSlot = slots[s];
|
|
1876
|
+
matches[s] = matches[s] || {
|
|
1877
|
+
index: accessPatternSlot.index,
|
|
1878
|
+
allKeys: false,
|
|
1879
|
+
hasSk: false,
|
|
1880
|
+
count: 0,
|
|
1881
|
+
done: false,
|
|
1882
|
+
keys: []
|
|
1914
1883
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1884
|
+
// already determined to be out of contention on prior iteration
|
|
1885
|
+
const indexOutOfContention = matches[s].done;
|
|
1886
|
+
// composite shorter than other indexes
|
|
1887
|
+
const lacksAttributeAtSlot = !accessPatternSlot;
|
|
1888
|
+
// attribute at this slot is not in the object provided
|
|
1889
|
+
const attributeNotProvided = accessPatternSlot && attributes[accessPatternSlot.name] === undefined;
|
|
1890
|
+
// if the next attribute is a sort key then all partition keys were provided
|
|
1891
|
+
const nextAttributeIsSortKey = accessPatternSlot && accessPatternSlot.next && facets[f+1][s].type === "sk";
|
|
1892
|
+
// if no keys are left then all attribute requirements were met (remember indexes don't require a sort key)
|
|
1893
|
+
const hasAllKeys = accessPatternSlot && !accessPatternSlot.next;
|
|
1894
|
+
|
|
1895
|
+
// no sense iterating on items we know to be "done"
|
|
1896
|
+
if (indexOutOfContention || lacksAttributeAtSlot || attributeNotProvided) {
|
|
1897
|
+
matches[s].done = true;
|
|
1919
1898
|
continue;
|
|
1920
|
-
} else {
|
|
1921
|
-
match = facets[i][currentMatches[0]].index;
|
|
1922
|
-
break;
|
|
1923
1899
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1900
|
+
|
|
1901
|
+
// if the next attribute is a sort key (and you reached this line) then you have fulfilled all the
|
|
1902
|
+
// partition key requirements for this index
|
|
1903
|
+
if (nextAttributeIsSortKey) {
|
|
1904
|
+
matches[s].hasSk = true;
|
|
1905
|
+
// if you reached this step and there are no more attributes, then you fulfilled the index
|
|
1906
|
+
} else if (hasAllKeys) {
|
|
1907
|
+
matches[s].allKeys = true;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// number of successfully fulfilled attributes plays into the ranking heuristic
|
|
1911
|
+
matches[s].count++;
|
|
1912
|
+
|
|
1913
|
+
// note the names/types of fulfilled attributes
|
|
1914
|
+
matches[s].keys.push({
|
|
1915
|
+
name: accessPatternSlot.name,
|
|
1916
|
+
type: accessPatternSlot.type
|
|
1917
|
+
});
|
|
1929
1918
|
}
|
|
1930
1919
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1920
|
+
// the highest count of matched attributes among all access patterns
|
|
1921
|
+
let max = 0;
|
|
1922
|
+
matches = matches
|
|
1923
|
+
// remove incomplete indexes
|
|
1924
|
+
.filter(match => match.hasSk || match.allKeys)
|
|
1925
|
+
// calculate max attribute match
|
|
1926
|
+
.map(match => {
|
|
1927
|
+
max = Math.max(max, match.count);
|
|
1928
|
+
return match;
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
// matched contains the ranked attributes. The closer an element is to zero the "higher" the rank.
|
|
1932
|
+
const matched = [];
|
|
1933
|
+
for (let m = 0; m < matches.length; m++) {
|
|
1934
|
+
const match = matches[m];
|
|
1935
|
+
// a finished primary index is most ideal (could be a get)
|
|
1936
|
+
const primaryIndexIsFinished = match.index === "" && match.allKeys;
|
|
1937
|
+
// if there is a tie for matched index attributes, primary index should win
|
|
1938
|
+
const primaryIndexIsMostMatched = match.index === "" && match.count === max;
|
|
1939
|
+
// composite attributes are complete
|
|
1940
|
+
const indexRequirementsFulfilled = match.allKeys;
|
|
1941
|
+
// having the most matches is important
|
|
1942
|
+
const hasTheMostAttributeMatches = match.count === max;
|
|
1943
|
+
if (primaryIndexIsFinished) {
|
|
1944
|
+
matched[0] = match;
|
|
1945
|
+
} else if (primaryIndexIsMostMatched) {
|
|
1946
|
+
matched[1] = match;
|
|
1947
|
+
} else if (indexRequirementsFulfilled) {
|
|
1948
|
+
matched[2] = match;
|
|
1949
|
+
} else if (hasTheMostAttributeMatches) {
|
|
1950
|
+
matched[3] = match;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
// find the first non-undefined element (best ranked) -- if possible
|
|
1954
|
+
const match = matched.find(value => !!value);
|
|
1955
|
+
let keys = [];
|
|
1956
|
+
let index = "";
|
|
1957
|
+
let shouldScan = true;
|
|
1958
|
+
if (match) {
|
|
1959
|
+
keys = match.keys;
|
|
1960
|
+
index = match.index;
|
|
1961
|
+
shouldScan = false;
|
|
1962
|
+
}
|
|
1963
|
+
return { keys, index, shouldScan };
|
|
1936
1964
|
}
|
|
1937
1965
|
|
|
1938
1966
|
/* istanbul ignore next */
|
|
@@ -1969,7 +1997,7 @@ class Entity {
|
|
|
1969
1997
|
type = "name";
|
|
1970
1998
|
} else if (char === "}" && type === "name") {
|
|
1971
1999
|
if (current.name.match(/^\s*$/)) {
|
|
1972
|
-
throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. Empty expression "\${${current.name}" provided. Expected attribute name.`);
|
|
2000
|
+
throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. Empty expression "\${${current.name}}" provided. Expected attribute name.`);
|
|
1973
2001
|
}
|
|
1974
2002
|
attributes.push({name: current.name, label: current.label});
|
|
1975
2003
|
current.name = "";
|
package/src/errors.js
CHANGED
|
@@ -207,28 +207,112 @@ const ErrorCodes = {
|
|
|
207
207
|
},
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
+
function makeMessage(message, section) {
|
|
211
|
+
return `${message} - For more detail on this error reference: ${getHelpLink(section)}`
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
class ElectroError extends Error {
|
|
211
|
-
constructor(
|
|
215
|
+
constructor(code, message) {
|
|
212
216
|
super(message);
|
|
213
217
|
let detail = ErrorCodes.UnknownError;
|
|
214
|
-
if (
|
|
215
|
-
detail =
|
|
218
|
+
if (code && code.sym === ErrorCode) {
|
|
219
|
+
detail = code
|
|
216
220
|
}
|
|
217
|
-
this.
|
|
218
|
-
|
|
221
|
+
this._message = message;
|
|
222
|
+
// this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`;
|
|
223
|
+
this.message = makeMessage(message, detail.section);
|
|
219
224
|
if (Error.captureStackTrace) {
|
|
220
225
|
Error.captureStackTrace(this, ElectroError);
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
this.name = 'ElectroError';
|
|
224
|
-
this.ref =
|
|
229
|
+
this.ref = code;
|
|
225
230
|
this.code = detail.code;
|
|
226
|
-
this.date =
|
|
231
|
+
this.date = Date.now();
|
|
227
232
|
this.isElectroError = true;
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
class ElectroValidationError extends ElectroError {
|
|
237
|
+
constructor(errors = []) {
|
|
238
|
+
const fields = [];
|
|
239
|
+
const messages = [];
|
|
240
|
+
for (let i = 0; i < errors.length; i++) {
|
|
241
|
+
const error = errors[i];
|
|
242
|
+
const message = error ? (error._message || error.message) : undefined;
|
|
243
|
+
messages.push(message);
|
|
244
|
+
if (error instanceof ElectroUserValidationError) {
|
|
245
|
+
fields.push({
|
|
246
|
+
field: error.field,
|
|
247
|
+
index: error.index,
|
|
248
|
+
reason: message,
|
|
249
|
+
cause: error.cause,
|
|
250
|
+
type: 'validation'
|
|
251
|
+
});
|
|
252
|
+
} else if (error instanceof ElectroAttributeValidationError) {
|
|
253
|
+
fields.push({
|
|
254
|
+
field: error.field,
|
|
255
|
+
index: error.index,
|
|
256
|
+
reason: message,
|
|
257
|
+
cause: error.cause || error, // error | undefined
|
|
258
|
+
type: 'validation'
|
|
259
|
+
});
|
|
260
|
+
} else if (message) {
|
|
261
|
+
fields.push({
|
|
262
|
+
field: '',
|
|
263
|
+
index: error.index,
|
|
264
|
+
reason: message,
|
|
265
|
+
cause: error !== undefined ? error.cause || error : undefined,
|
|
266
|
+
type: 'fatal'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const message = messages
|
|
272
|
+
.filter(message => typeof message === "string" && message.length)
|
|
273
|
+
.join(', ') || `Invalid value(s) provided`;
|
|
274
|
+
|
|
275
|
+
super(ErrorCodes.InvalidAttribute, message);
|
|
276
|
+
this.fields = fields;
|
|
277
|
+
this.name = "ElectroValidationError";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class ElectroUserValidationError extends ElectroError {
|
|
282
|
+
constructor(field, cause) {
|
|
283
|
+
let message;
|
|
284
|
+
let hasCause = false;
|
|
285
|
+
if (typeof cause === "string") {
|
|
286
|
+
message = cause;
|
|
287
|
+
} else if (cause !== undefined && typeof cause._message === "string" && cause._message.length) {
|
|
288
|
+
message = cause._message;
|
|
289
|
+
hasCause = true;
|
|
290
|
+
} else if (cause !== undefined && typeof cause.message === "string" && cause.message.length) {
|
|
291
|
+
message = cause.message;
|
|
292
|
+
hasCause = true;
|
|
293
|
+
} else {
|
|
294
|
+
message = "Invalid value provided";
|
|
295
|
+
}
|
|
296
|
+
super(ErrorCodes.InvalidAttribute, message);
|
|
297
|
+
this.field = field;
|
|
298
|
+
this.name = "ElectroUserValidationError";
|
|
299
|
+
if (hasCause) {
|
|
300
|
+
this.cause = cause;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
class ElectroAttributeValidationError extends ElectroError {
|
|
306
|
+
constructor(field, reason) {
|
|
307
|
+
super(ErrorCodes.InvalidAttribute, reason);
|
|
308
|
+
this.field = field;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
231
312
|
module.exports = {
|
|
313
|
+
ErrorCodes,
|
|
232
314
|
ElectroError,
|
|
233
|
-
|
|
315
|
+
ElectroValidationError,
|
|
316
|
+
ElectroUserValidationError,
|
|
317
|
+
ElectroAttributeValidationError
|
|
234
318
|
};
|
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/schema.js
CHANGED
|
@@ -289,7 +289,7 @@ class Attribute {
|
|
|
289
289
|
|
|
290
290
|
_makeCast(name, cast) {
|
|
291
291
|
if (cast !== undefined && !CastTypes.includes(cast)) {
|
|
292
|
-
throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", "
|
|
292
|
+
throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", ")}`,
|
|
293
293
|
);
|
|
294
294
|
} else if (cast === AttributeTypes.string) {
|
|
295
295
|
return (val) => {
|
|
@@ -327,20 +327,34 @@ class Attribute {
|
|
|
327
327
|
_makeValidate(definition) {
|
|
328
328
|
if (typeof definition === "function") {
|
|
329
329
|
return (val) => {
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
try {
|
|
331
|
+
let reason = definition(val);
|
|
332
|
+
const isValid = !reason;
|
|
333
|
+
if (isValid) {
|
|
334
|
+
return [isValid, []];
|
|
335
|
+
} else if (typeof reason === "boolean") {
|
|
336
|
+
return [isValid, [new e.ElectroUserValidationError(this.path, "Invalid value provided")]];
|
|
337
|
+
} else {
|
|
338
|
+
return [isValid, [new e.ElectroUserValidationError(this.path, reason)]];
|
|
339
|
+
}
|
|
340
|
+
} catch(err) {
|
|
341
|
+
return [false, [new e.ElectroUserValidationError(this.path, err)]];
|
|
342
|
+
}
|
|
332
343
|
};
|
|
333
344
|
} else if (definition instanceof RegExp) {
|
|
334
345
|
return (val) => {
|
|
335
346
|
if (val === undefined) {
|
|
336
|
-
return [true,
|
|
347
|
+
return [true, []];
|
|
337
348
|
}
|
|
338
349
|
let isValid = definition.test(val);
|
|
339
|
-
let reason =
|
|
350
|
+
let reason = [];
|
|
351
|
+
if (!isValid) {
|
|
352
|
+
reason.push(new e.ElectroUserValidationError(this.path, `Invalid value for attribute "${this.path}": Failed model defined regex`));
|
|
353
|
+
}
|
|
340
354
|
return [isValid, reason];
|
|
341
355
|
};
|
|
342
356
|
} else {
|
|
343
|
-
return (
|
|
357
|
+
return () => [true, []];
|
|
344
358
|
}
|
|
345
359
|
}
|
|
346
360
|
|
|
@@ -385,15 +399,19 @@ class Attribute {
|
|
|
385
399
|
|
|
386
400
|
_isType(value) {
|
|
387
401
|
if (value === undefined) {
|
|
388
|
-
|
|
402
|
+
let reason = [];
|
|
403
|
+
if (this.required) {
|
|
404
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
|
|
405
|
+
}
|
|
406
|
+
return [!this.required, reason];
|
|
389
407
|
}
|
|
390
408
|
let isTyped = false;
|
|
391
|
-
let reason =
|
|
409
|
+
let reason = [];
|
|
392
410
|
switch (this.type) {
|
|
393
411
|
case AttributeTypes.enum:
|
|
394
412
|
isTyped = this.enumArray.includes(value);
|
|
395
413
|
if (!isTyped) {
|
|
396
|
-
reason
|
|
414
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value not found in set of acceptable values: ${u.commaSeparatedString(this.enumArray)}`));
|
|
397
415
|
}
|
|
398
416
|
break;
|
|
399
417
|
case AttributeTypes.any:
|
|
@@ -405,7 +423,7 @@ class Attribute {
|
|
|
405
423
|
default:
|
|
406
424
|
isTyped = typeof value === this.type;
|
|
407
425
|
if (!isTyped) {
|
|
408
|
-
reason
|
|
426
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Received value of type "${typeof value}", expected value of type "${this.type}"`));
|
|
409
427
|
}
|
|
410
428
|
break;
|
|
411
429
|
}
|
|
@@ -414,12 +432,12 @@ class Attribute {
|
|
|
414
432
|
|
|
415
433
|
isValid(value) {
|
|
416
434
|
try {
|
|
417
|
-
let [isTyped,
|
|
418
|
-
let [isValid, validationError] = this.validate(value);
|
|
419
|
-
let
|
|
420
|
-
return [isTyped && isValid,
|
|
435
|
+
let [isTyped, typeErrorReason] = this._isType(value);
|
|
436
|
+
let [isValid, validationError] = isTyped ? this.validate(value) : [false, []];
|
|
437
|
+
let errors = [...typeErrorReason, ...validationError].filter(value => value !== undefined);
|
|
438
|
+
return [isTyped && isValid, errors];
|
|
421
439
|
} catch (err) {
|
|
422
|
-
return [false, err
|
|
440
|
+
return [false, [err]];
|
|
423
441
|
}
|
|
424
442
|
}
|
|
425
443
|
|
|
@@ -433,10 +451,9 @@ class Attribute {
|
|
|
433
451
|
|
|
434
452
|
getValidate(value) {
|
|
435
453
|
value = this.val(value);
|
|
436
|
-
let [isValid,
|
|
454
|
+
let [isValid, validationErrors] = this.isValid(value);
|
|
437
455
|
if (!isValid) {
|
|
438
|
-
|
|
439
|
-
throw new Error(validationError);
|
|
456
|
+
throw new e.ElectroValidationError(validationErrors);
|
|
440
457
|
}
|
|
441
458
|
return value;
|
|
442
459
|
}
|
|
@@ -509,13 +526,17 @@ class MapAttribute extends Attribute {
|
|
|
509
526
|
|
|
510
527
|
_isType(value) {
|
|
511
528
|
if (value === undefined) {
|
|
512
|
-
|
|
529
|
+
let reason = [];
|
|
530
|
+
if (this.required) {
|
|
531
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
|
|
532
|
+
}
|
|
533
|
+
return [!this.required, reason];
|
|
513
534
|
}
|
|
514
535
|
const valueType = getValueType(value);
|
|
515
536
|
if (valueType !== ValueTypes.object) {
|
|
516
|
-
return [false, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`];
|
|
537
|
+
return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`)]];
|
|
517
538
|
}
|
|
518
|
-
let reason =
|
|
539
|
+
let reason = [];
|
|
519
540
|
const [childrenAreValid, childErrors] = this._validateChildren(value);
|
|
520
541
|
if (!childrenAreValid) {
|
|
521
542
|
reason = childErrors;
|
|
@@ -526,24 +547,24 @@ class MapAttribute extends Attribute {
|
|
|
526
547
|
_validateChildren(value) {
|
|
527
548
|
const valueType = getValueType(value);
|
|
528
549
|
const attributes = this.properties.attributes;
|
|
529
|
-
|
|
550
|
+
let errors = [];
|
|
530
551
|
if (valueType === ValueTypes.object) {
|
|
531
552
|
for (const child of Object.keys(attributes)) {
|
|
532
|
-
const [isValid,
|
|
553
|
+
const [isValid, errorValues] = attributes[child].isValid(value === undefined ? value : value[child])
|
|
533
554
|
if (!isValid) {
|
|
534
|
-
errors
|
|
555
|
+
errors = [...errors, ...errorValues]
|
|
535
556
|
}
|
|
536
557
|
}
|
|
537
558
|
} else if (valueType !== ValueTypes.object) {
|
|
538
559
|
errors.push(
|
|
539
|
-
`Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`
|
|
560
|
+
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`)
|
|
540
561
|
);
|
|
541
562
|
} else if (this.properties.hasRequiredAttributes) {
|
|
542
563
|
errors.push(
|
|
543
|
-
`Invalid value type at entity path: "${this.path}". Map attribute requires at least the properties ${u.commaSeparatedString(Object.keys(attributes))}`
|
|
564
|
+
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Map attribute requires at least the properties ${u.commaSeparatedString(Object.keys(attributes))}`)
|
|
544
565
|
);
|
|
545
566
|
}
|
|
546
|
-
return [errors.length === 0, errors
|
|
567
|
+
return [errors.length === 0, errors];
|
|
547
568
|
}
|
|
548
569
|
|
|
549
570
|
val(value) {
|
|
@@ -560,7 +581,7 @@ class MapAttribute extends Attribute {
|
|
|
560
581
|
} else if (value && valueType !== "object" && Object.keys(value).length === 0) {
|
|
561
582
|
return getValue(value);
|
|
562
583
|
} else if (valueType !== "object") {
|
|
563
|
-
throw new
|
|
584
|
+
throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`);
|
|
564
585
|
}
|
|
565
586
|
|
|
566
587
|
const data = {};
|
|
@@ -643,24 +664,29 @@ class ListAttribute extends Attribute {
|
|
|
643
664
|
}
|
|
644
665
|
|
|
645
666
|
_validateArrayValue(value) {
|
|
667
|
+
const reason = [];
|
|
646
668
|
const valueType = getValueType(value);
|
|
647
669
|
if (value !== undefined && valueType !== ValueTypes.array) {
|
|
648
|
-
return [false, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`];
|
|
670
|
+
return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`)]];
|
|
649
671
|
} else {
|
|
650
|
-
return [true,
|
|
672
|
+
return [true, []];
|
|
651
673
|
}
|
|
652
674
|
}
|
|
653
675
|
|
|
654
676
|
_isType(value) {
|
|
655
677
|
if (value === undefined) {
|
|
656
|
-
|
|
678
|
+
let reason = [];
|
|
679
|
+
if (this.required) {
|
|
680
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
|
|
681
|
+
}
|
|
682
|
+
return [!this.required, reason];
|
|
657
683
|
}
|
|
658
684
|
|
|
659
685
|
const [isValidArray, errors] = this._validateArrayValue(value);
|
|
660
686
|
if (!isValidArray) {
|
|
661
687
|
return [isValidArray, errors];
|
|
662
688
|
}
|
|
663
|
-
let reason =
|
|
689
|
+
let reason = [];
|
|
664
690
|
const [childrenAreValid, childErrors] = this._validateChildren(value);
|
|
665
691
|
if (!childrenAreValid) {
|
|
666
692
|
reason = childErrors;
|
|
@@ -673,17 +699,22 @@ class ListAttribute extends Attribute {
|
|
|
673
699
|
const errors = [];
|
|
674
700
|
if (valueType === ValueTypes.array) {
|
|
675
701
|
for (const i in value) {
|
|
676
|
-
const [isValid,
|
|
702
|
+
const [isValid, errorValues] = this.items.isValid(value[i]);
|
|
677
703
|
if (!isValid) {
|
|
678
|
-
|
|
704
|
+
for (const err of errorValues) {
|
|
705
|
+
if (err instanceof e.ElectroAttributeValidationError || err instanceof e.ElectroUserValidationError) {
|
|
706
|
+
err.index = parseInt(i);
|
|
707
|
+
}
|
|
708
|
+
errors.push(err);
|
|
709
|
+
}
|
|
679
710
|
}
|
|
680
711
|
}
|
|
681
712
|
} else {
|
|
682
713
|
errors.push(
|
|
683
|
-
`Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"`
|
|
714
|
+
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"`)
|
|
684
715
|
);
|
|
685
716
|
}
|
|
686
|
-
return [errors.length === 0, errors
|
|
717
|
+
return [errors.length === 0, errors];
|
|
687
718
|
}
|
|
688
719
|
|
|
689
720
|
val(value) {
|
|
@@ -700,7 +731,7 @@ class ListAttribute extends Attribute {
|
|
|
700
731
|
} else if (Array.isArray(value) && value.length === 0) {
|
|
701
732
|
return value;
|
|
702
733
|
} else if (!Array.isArray(value)) {
|
|
703
|
-
throw new
|
|
734
|
+
throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${getValueType(value)}", expected value of type "array"`);
|
|
704
735
|
}
|
|
705
736
|
|
|
706
737
|
const data = [];
|
|
@@ -731,6 +762,20 @@ class SetAttribute extends Attribute {
|
|
|
731
762
|
this.items = items;
|
|
732
763
|
this.get = this._makeGet(definition.get, items);
|
|
733
764
|
this.set = this._makeSet(definition.set, items);
|
|
765
|
+
this.validate = this._makeSetValidate(definition);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
_makeSetValidate(definition) {
|
|
769
|
+
const validate = this._makeValidate(definition.validate);
|
|
770
|
+
return (value) => {
|
|
771
|
+
if (Array.isArray(value)) {
|
|
772
|
+
return validate([...value]);
|
|
773
|
+
} else if (value && value.wrapperName === 'Set') {
|
|
774
|
+
return validate([...value.values])
|
|
775
|
+
} else {
|
|
776
|
+
return validate(value);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
734
779
|
}
|
|
735
780
|
|
|
736
781
|
fromDDBSet(value) {
|
|
@@ -768,7 +813,7 @@ class SetAttribute extends Attribute {
|
|
|
768
813
|
return this._createDDBSet(value);
|
|
769
814
|
}
|
|
770
815
|
default:
|
|
771
|
-
throw new
|
|
816
|
+
throw new e.ElectroAttributeValidationError(this.path, `Invalid attribute value supplied to "set" attribute "${this.path}". Received value of type "${valueType}". Set values must be supplied as either Arrays, native JavaScript Set objects, DocumentClient Set objects, strings, or numbers.`)
|
|
772
817
|
}
|
|
773
818
|
|
|
774
819
|
}
|
|
@@ -795,7 +840,9 @@ class SetAttribute extends Attribute {
|
|
|
795
840
|
this._checkGetSet(set, "set");
|
|
796
841
|
const setter = set || ((attr) => attr);
|
|
797
842
|
return (values, siblings) => {
|
|
798
|
-
const results =
|
|
843
|
+
const results = values && values.wrapperName === 'Set'
|
|
844
|
+
? setter(values.values, siblings)
|
|
845
|
+
: setter(values, siblings)
|
|
799
846
|
if (results !== undefined) {
|
|
800
847
|
return this.toDDBSet(results);
|
|
801
848
|
}
|
|
@@ -804,10 +851,14 @@ class SetAttribute extends Attribute {
|
|
|
804
851
|
|
|
805
852
|
_isType(value) {
|
|
806
853
|
if (value === undefined) {
|
|
807
|
-
|
|
854
|
+
const reason = [];
|
|
855
|
+
if (this.required) {
|
|
856
|
+
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
|
|
857
|
+
}
|
|
858
|
+
return [!this.required, reason];
|
|
808
859
|
}
|
|
809
860
|
|
|
810
|
-
let reason =
|
|
861
|
+
let reason = [];
|
|
811
862
|
const [childrenAreValid, childErrors] = this._validateChildren(value);
|
|
812
863
|
if (!childrenAreValid) {
|
|
813
864
|
reason = childErrors;
|
|
@@ -817,7 +868,7 @@ class SetAttribute extends Attribute {
|
|
|
817
868
|
|
|
818
869
|
_validateChildren(value) {
|
|
819
870
|
const valueType = getValueType(value);
|
|
820
|
-
|
|
871
|
+
let errors = [];
|
|
821
872
|
let arr = [];
|
|
822
873
|
if (valueType === ValueTypes.array) {
|
|
823
874
|
arr = value;
|
|
@@ -827,16 +878,16 @@ class SetAttribute extends Attribute {
|
|
|
827
878
|
arr = value.values;
|
|
828
879
|
} else {
|
|
829
880
|
errors.push(
|
|
830
|
-
`Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"`
|
|
881
|
+
new e.ElectroAttributeValidationError(this.path, `Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"`)
|
|
831
882
|
)
|
|
832
883
|
}
|
|
833
884
|
for (const item of arr) {
|
|
834
|
-
const [isValid,
|
|
885
|
+
const [isValid, errorValues] = this.items.isValid(item);
|
|
835
886
|
if (!isValid) {
|
|
836
|
-
errors
|
|
887
|
+
errors = [...errors, ...errorValues];
|
|
837
888
|
}
|
|
838
889
|
}
|
|
839
|
-
return [errors.length === 0, errors
|
|
890
|
+
return [errors.length === 0, errors];
|
|
840
891
|
}
|
|
841
892
|
|
|
842
893
|
val(value) {
|
|
@@ -1232,11 +1283,11 @@ class Schema {
|
|
|
1232
1283
|
for (const path of paths) {
|
|
1233
1284
|
const attribute = this.traverser.getPath(path);
|
|
1234
1285
|
if (!attribute) {
|
|
1235
|
-
throw new
|
|
1286
|
+
throw new e.ElectroAttributeValidationError(path, `Attribute "${path}" does not exist on model.`);
|
|
1236
1287
|
} else if (attribute.readOnly) {
|
|
1237
|
-
throw new
|
|
1288
|
+
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be removed`);
|
|
1238
1289
|
} else if (attribute.required) {
|
|
1239
|
-
throw new
|
|
1290
|
+
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`);
|
|
1240
1291
|
}
|
|
1241
1292
|
}
|
|
1242
1293
|
return paths;
|
|
@@ -1251,7 +1302,7 @@ class Schema {
|
|
|
1251
1302
|
}
|
|
1252
1303
|
if (attribute.readOnly) {
|
|
1253
1304
|
// todo: #electroerror
|
|
1254
|
-
throw new
|
|
1305
|
+
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`);
|
|
1255
1306
|
} else {
|
|
1256
1307
|
record[path] = attribute.getValidate(value);
|
|
1257
1308
|
}
|
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
|
}
|