electrodb 2.0.0 → 2.1.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/README.md +27 -21
- package/index.d.ts +12 -0
- package/package.json +1 -1
- package/src/entity.js +8 -3
- package/src/operations.js +1 -1
- package/src/schema.js +68 -12
- package/src/service.js +18 -6
- package/src/util.js +39 -0
- package/src/validations.js +12 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
------------
|
|
13
13
|
|
|
14
14
|
<h1 align="center">ElectroDB has now reached 2.0.0!</h1>
|
|
15
|
+
|
|
15
16
|
For existing users, checkout the [CHANGELOG](./CHANGELOG.md) and/or the section [Version 2 Migration](#version-2-migration) to learn more about the recent move to 2.0.0 and the changes neccessary to move to the newest version.
|
|
16
17
|
|
|
17
18
|
------------
|
|
@@ -344,7 +345,7 @@ import { Entity } from "electrodb";
|
|
|
344
345
|
> 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.
|
|
345
346
|
|
|
346
347
|
# Services
|
|
347
|
-
In ***ElectroDB*** a `Service` represents a collection of related Entities. Services allow you to build queries span across Entities. Similar to Entities, Services can coexist on a single table without collision. You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries that `join` multiple Entities.
|
|
348
|
+
In ***ElectroDB*** a `Service` represents a collection of related Entities. Services allow you to build queries that span across Entities. Similar to Entities, Services can coexist on a single table without collision. You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries that `join` multiple Entities.
|
|
348
349
|
|
|
349
350
|
Require:
|
|
350
351
|
```javascript
|
|
@@ -628,7 +629,11 @@ attributes: {
|
|
|
628
629
|
cast?: "number"|"string"|"boolean";
|
|
629
630
|
get?: (attribute: <type>, schema: any) => <type> | void | undefined;
|
|
630
631
|
set?: (attribute?: <type>, schema?: any) => <type> | void | undefined;
|
|
631
|
-
watch
|
|
632
|
+
watch?: "*" | string[];
|
|
633
|
+
padding?: {
|
|
634
|
+
length: number;
|
|
635
|
+
char: string;
|
|
636
|
+
}
|
|
632
637
|
}
|
|
633
638
|
}
|
|
634
639
|
```
|
|
@@ -637,22 +642,23 @@ attributes: {
|
|
|
637
642
|
|
|
638
643
|
#### Attribute Definition
|
|
639
644
|
|
|
640
|
-
Property | Type | Required | Types
|
|
641
|
-
------------ | :--------------------------------------------------------: | :------: |
|
|
642
|
-
`type` | `string`, `ReadonlyArray<string>`, `string[]` | yes | all
|
|
643
|
-
`required` | `boolean` | no | all
|
|
644
|
-
`hidden` | `boolean` | no | all
|
|
645
|
-
`default` | `value`, `() => value` | no | all
|
|
646
|
-
`validate` | `RegExp`, `(value: any) => void`, `(value: any) => string` | no | all
|
|
647
|
-
`field` | `string` | no | all
|
|
648
|
-
`readOnly` | `boolean` | no | all
|
|
649
|
-
`label` | `string` | no | all
|
|
650
|
-
`
|
|
651
|
-
`
|
|
652
|
-
`
|
|
653
|
-
`
|
|
654
|
-
`
|
|
655
|
-
`items` |
|
|
645
|
+
Property | Type | Required | Types | Description
|
|
646
|
+
------------ | :--------------------------------------------------------: | :------: | :------------: | -----------
|
|
647
|
+
`type` | `string`, `ReadonlyArray<string>`, `string[]` | yes | all | Accepts the values: `"string"`, `"number"` `"boolean"`, `"map"`, `"list"`, `"set"`, an array of strings representing a finite list of acceptable values: `["option1", "option2", "option3"]`, or `"any"` which disables value type checking on that attribute.
|
|
648
|
+
`required` | `boolean` | no | all | Flag an attribute as required to be present when creating a record. This attribute also acts as a type of `NOT NULL` flag, preventing it from being removed directly. When applied to nested properties, be mindful that default map values can cause required child attributes to fail validation.
|
|
649
|
+
`hidden` | `boolean` | no | all | Flag an attribute as hidden to remove the property from results before they are returned.
|
|
650
|
+
`default` | `value`, `() => value` | no | all | Either the default value itself or a synchronous function that returns the desired value. Applied before `set` and before `required` check. In the case of nested attributes, default values will apply defaults to children attributes until an undefined value is reached
|
|
651
|
+
`validate` | `RegExp`, `(value: any) => void`, `(value: any) => string` | no | all | Either regex or a synchronous callback to return an error string (will result in exception using the string as the error's message), or thrown exception in the event of an error.
|
|
652
|
+
`field` | `string` | no | all | The name of the attribute as it exists in DynamoDB, if named differently in the schema attributes. Defaults to the `AttributeName` as defined in the schema.
|
|
653
|
+
`readOnly` | `boolean` | no | all | Prevents an attribute from being updated after the record has been created. Attributes used in the composition of the table's primary Partition Key and Sort Key are read-only by default. The one exception to `readOnly` is for properties that also use the `watch` property, read [attribute watching](#attribute-watching) for more detail.
|
|
654
|
+
`label` | `string` | no | all | Used in index key composition to prefix key composite attributes. By default, the `AttributeName` is used as the label.
|
|
655
|
+
`padding` | `{ length: number; char: string; }` | no | string, number | Similar to `label`, this property only impacts the attribute's value during index key composition. Padding allows you to define a string pattern to left pad your attribute when ElectroDB builds your partition or sort key. This can be helpful to implementing zero-padding patterns with numbers and strings in sort keys. Note, this will _not_ impact your attribute's stored value, if you want to transform the attribute's field value, use the `set` callback described below.
|
|
656
|
+
`set` | `(attribute, schema) => value` | no | all | A synchronous callback allowing you to apply changes to a value before it is set in params or applied to the database. First value represents the value passed to ElectroDB, second value are the attributes passed on that update/put
|
|
657
|
+
`get` | `(attribute, schema) => value` | no | all | A synchronous callback allowing you to apply changes to a value after it is retrieved from the database. First value represents the value passed to ElectroDB, second value are the attributes retrieved from the database.
|
|
658
|
+
`watch` | `Attribute[], "*"` | no | root-only | Define other attributes that will always trigger your attribute's getter and setter callback after their getter/setter callbacks are executed. Only available on root level attributes.
|
|
659
|
+
`properties` | `{[key: string]: Attribute}` | yes* | map | Define the properties available on a `"map"` attribute, required if your attribute is a map. Syntax for map properties is the same as root level attributes.
|
|
660
|
+
`items` | `Attribute` | yes* | list | Define the attribute type your list attribute will contain, required if your attribute is a list. Syntax for list items is the same as a single attribute.
|
|
661
|
+
`items` | "string" | "number" | yes* | set | Define the primitive type your set attribute will contain, required if your attribute is a set. Unlike lists, a set defines it's items with a string of either "string" or "number".
|
|
656
662
|
|
|
657
663
|
#### Enum Attributes
|
|
658
664
|
|
|
@@ -2643,8 +2649,7 @@ Equivalent DocClient Parameters:
|
|
|
2643
2649
|
### Batch Get
|
|
2644
2650
|
Provide all Table Index composite attributes in an array of objects to the `get` method to perform a BatchGet query.
|
|
2645
2651
|
|
|
2646
|
-
> _NOTE:
|
|
2647
|
-
> Additionally, when performing a BatchGet the `.params()` method will return an _array_ of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchGet queries larger than the docClient's limit of 100 records.
|
|
2652
|
+
> _NOTE: When performing a BatchGet the `.params()` method will return an _array_ of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchGet allows queries larger than the docClient's limit of 100 records.
|
|
2648
2653
|
|
|
2649
2654
|
If the number of records you are requesting is above the BatchGet threshold of 100 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the [Query Option](#query-options) `concurrent`. This value can be set to any number greater than zero, and will execute that number of requests simultaneously.
|
|
2650
2655
|
|
|
@@ -4258,6 +4263,7 @@ By default, **ElectroDB** enables you to work with records as the names and prop
|
|
|
4258
4263
|
listeners Array<(event) => void>;
|
|
4259
4264
|
preserveBatchOrder?: boolean;
|
|
4260
4265
|
attributes?: string[];
|
|
4266
|
+
order?: 'asc' | 'desc';
|
|
4261
4267
|
};
|
|
4262
4268
|
```
|
|
4263
4269
|
|
|
@@ -4275,7 +4281,7 @@ response | `"default"` | Used as a convenience for applying t
|
|
|
4275
4281
|
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`.
|
|
4276
4282
|
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.
|
|
4277
4283
|
pages | 1 | How many DynamoDB pages should a query iterate through before stopping. To have ElectroDB automatically paginate through all results, pass the string value `'all'`.
|
|
4278
|
-
|
|
4284
|
+
order | 'asc' | Convenience option for `ScanIndexForward`, to the change the order of queries based on your index's Sort Key -- valid options include 'asc' and 'desc'. [[read more](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html)]
|
|
4279
4285
|
listeners | `[]` | An array of callbacks that are invoked when [internal ElectroDB events](#events) occur.
|
|
4280
4286
|
logger | _none_ | A convenience option for a single event listener that semantically can be used for logging.
|
|
4281
4287
|
preserveBatchOrder | `false` | When used with a [batchGet](#batch-get) operation, ElectroDB will ensure the order returned by a batchGet will be the same as the order provided. When enabled, if a record is returned from DynamoDB as "unprocessed" ([read more here](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)), ElectroDB will return a null value at that index.
|
package/index.d.ts
CHANGED
|
@@ -781,6 +781,10 @@ export interface BooleanAttribute {
|
|
|
781
781
|
readonly field?: string;
|
|
782
782
|
readonly label?: string;
|
|
783
783
|
readonly watch?: ReadonlyArray<string> | "*";
|
|
784
|
+
readonly padding?: {
|
|
785
|
+
length: number;
|
|
786
|
+
char: string;
|
|
787
|
+
}
|
|
784
788
|
}
|
|
785
789
|
|
|
786
790
|
export interface NestedNumberAttribute {
|
|
@@ -807,6 +811,10 @@ export interface NumberAttribute {
|
|
|
807
811
|
readonly field?: string;
|
|
808
812
|
readonly label?: string;
|
|
809
813
|
readonly watch?: ReadonlyArray<string> | "*";
|
|
814
|
+
readonly padding?: {
|
|
815
|
+
length: number;
|
|
816
|
+
char: string;
|
|
817
|
+
}
|
|
810
818
|
}
|
|
811
819
|
|
|
812
820
|
export interface NestedStringAttribute {
|
|
@@ -833,6 +841,10 @@ export interface StringAttribute {
|
|
|
833
841
|
readonly field?: string;
|
|
834
842
|
readonly label?: string;
|
|
835
843
|
readonly watch?: ReadonlyArray<string> | "*";
|
|
844
|
+
readonly padding?: {
|
|
845
|
+
length: number;
|
|
846
|
+
char: string;
|
|
847
|
+
}
|
|
836
848
|
}
|
|
837
849
|
|
|
838
850
|
export interface NestedEnumAttribute {
|
package/package.json
CHANGED
package/src/entity.js
CHANGED
|
@@ -2178,12 +2178,17 @@ class Entity {
|
|
|
2178
2178
|
}
|
|
2179
2179
|
let key = prefix;
|
|
2180
2180
|
for (let i = 0; i < labels.length; i++) {
|
|
2181
|
-
|
|
2182
|
-
|
|
2181
|
+
const { name, label } = labels[i];
|
|
2182
|
+
const attribute = this.model.schema.getAttribute(name);
|
|
2183
|
+
let value = supplied[name];
|
|
2183
2184
|
if (supplied[name] === undefined && excludeLabelTail) {
|
|
2184
2185
|
break;
|
|
2185
2186
|
}
|
|
2186
2187
|
|
|
2188
|
+
if (attribute && validations.isFunction(attribute.format)) {
|
|
2189
|
+
value = attribute.format(`${value}`);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2187
2192
|
if (isCustom) {
|
|
2188
2193
|
key = `${key}${label}`;
|
|
2189
2194
|
} else {
|
|
@@ -2194,7 +2199,7 @@ class Entity {
|
|
|
2194
2199
|
break;
|
|
2195
2200
|
}
|
|
2196
2201
|
|
|
2197
|
-
key = `${key}${
|
|
2202
|
+
key = `${key}${value}`;
|
|
2198
2203
|
}
|
|
2199
2204
|
|
|
2200
2205
|
return u.formatKeyCasing(key, casing);
|
package/src/operations.js
CHANGED
|
@@ -388,7 +388,7 @@ class AttributeOperationProxy {
|
|
|
388
388
|
const attributeValues = [];
|
|
389
389
|
let hasNestedValue = false;
|
|
390
390
|
for (let value of values) {
|
|
391
|
-
value = target.
|
|
391
|
+
value = target.applyFixings(value);
|
|
392
392
|
// template.length is to see if function takes value argument
|
|
393
393
|
if (template.length > 3) {
|
|
394
394
|
if (seen.has(value)) {
|
package/src/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes } = require("./types");
|
|
1
|
+
const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TableIndex } = require("./types");
|
|
2
2
|
const AttributeTypeNames = Object.keys(AttributeTypes);
|
|
3
3
|
const ValidFacetTypes = [AttributeTypes.string, AttributeTypes.number, AttributeTypes.boolean, AttributeTypes.enum];
|
|
4
4
|
const e = require("./errors");
|
|
@@ -100,6 +100,9 @@ class Attribute {
|
|
|
100
100
|
this.isKeyField = !!definition.isKeyField;
|
|
101
101
|
this.unformat = this._makeDestructureKey(definition);
|
|
102
102
|
this.format = this._makeStructureKey(definition);
|
|
103
|
+
this.padding = definition.padding;
|
|
104
|
+
this.applyFixings = this._makeApplyFixings(definition);
|
|
105
|
+
this.applyPadding = this._makePadding(definition);
|
|
103
106
|
this.indexes = [...(definition.indexes || [])];
|
|
104
107
|
let {isWatched, isWatcher, watchedBy, watching, watchAll} = Attribute._destructureWatcher(definition);
|
|
105
108
|
this._isWatched = isWatched
|
|
@@ -233,29 +236,72 @@ class Attribute {
|
|
|
233
236
|
return set || ((attr) => attr);
|
|
234
237
|
}
|
|
235
238
|
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
value = `${prefix}${key}${postfix}`;
|
|
239
|
+
_makeApplyFixings({ prefix = "", postfix = "", casing= KeyCasing.none } = {}) {
|
|
240
|
+
return (value) => {
|
|
241
|
+
if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type)) {
|
|
242
|
+
value = `${prefix}${value}${postfix}`;
|
|
241
243
|
}
|
|
244
|
+
|
|
242
245
|
return u.formatAttributeCasing(value, casing);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
|
|
249
|
+
_makeStructureKey() {
|
|
250
|
+
return (key) => {
|
|
251
|
+
return this.applyPadding(key);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_isPaddingEligible(padding = {} ) {
|
|
256
|
+
return !!padding && padding.length && v.isStringHasLength(padding.char);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_makePadding({ padding = {} }) {
|
|
260
|
+
return (value) => {
|
|
261
|
+
if (typeof value !== 'string') {
|
|
262
|
+
return value;
|
|
263
|
+
} else if (this._isPaddingEligible(padding)) {
|
|
264
|
+
return u.addPadding({padding, value});
|
|
265
|
+
} else {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_makeRemoveFixings({prefix = "", postfix = "", casing= KeyCasing.none} = {}) {
|
|
247
272
|
return (key) => {
|
|
248
273
|
let value = "";
|
|
249
274
|
if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") {
|
|
250
|
-
|
|
251
|
-
} else if (key.length > prefix.length) {
|
|
275
|
+
value = key;
|
|
276
|
+
} else if (prefix.length > 0 && key.length > prefix.length) {
|
|
252
277
|
for (let i = prefix.length; i < key.length - postfix.length; i++) {
|
|
253
278
|
value += key[i];
|
|
254
279
|
}
|
|
255
280
|
} else {
|
|
256
281
|
value = key;
|
|
257
282
|
}
|
|
258
|
-
|
|
283
|
+
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_makeDestructureKey({prefix = "", postfix = "", casing= KeyCasing.none, padding = {}} = {}) {
|
|
289
|
+
return (key) => {
|
|
290
|
+
let value = "";
|
|
291
|
+
if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") {
|
|
292
|
+
return key;
|
|
293
|
+
} else if (key.length > prefix.length) {
|
|
294
|
+
value = u.removeFixings({prefix, postfix, value: key});
|
|
295
|
+
} else {
|
|
296
|
+
value = key;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// todo: if an attribute is also used as a pk or sk directly in one index, but a composite in another, then padding is going to be broken
|
|
300
|
+
// if (padding && padding.length) {
|
|
301
|
+
// value = u.removePadding({padding, value});
|
|
302
|
+
// }
|
|
303
|
+
|
|
304
|
+
return value;
|
|
259
305
|
};
|
|
260
306
|
}
|
|
261
307
|
|
|
@@ -958,7 +1004,7 @@ class Schema {
|
|
|
958
1004
|
let definition = facets.byField[field][indexName];
|
|
959
1005
|
if (definition.facets.length > 1) {
|
|
960
1006
|
throw new e.ElectroError(
|
|
961
|
-
e.ErrorCodes.
|
|
1007
|
+
e.ErrorCodes.InvalidIndexWithAttributeName,
|
|
962
1008
|
`Invalid definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore is not allowed to contain composite references to other attributes. Please either change the field name of the attribute, or redefine the index to use only the single attribute "${definition.field}".`
|
|
963
1009
|
)
|
|
964
1010
|
}
|
|
@@ -1005,10 +1051,19 @@ class Schema {
|
|
|
1005
1051
|
`Invalid use of a collection on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore the index is not allowed to participate in a Collection. Please either change the field name of the attribute, or remove all collection(s) from the index.`
|
|
1006
1052
|
)
|
|
1007
1053
|
}
|
|
1054
|
+
|
|
1055
|
+
if (definition.field === field) {
|
|
1056
|
+
if (attribute.padding !== undefined) {
|
|
1057
|
+
throw new e.ElectroError(
|
|
1058
|
+
e.ErrorCodes.InvalidAttributeDefinition,
|
|
1059
|
+
`Invalid padding definition for the attribute "${name}". Padding is not currently supported for attributes that are also defined as table indexes.`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1008
1063
|
}
|
|
1009
1064
|
}
|
|
1010
1065
|
|
|
1011
|
-
let isKey = !!facets.byIndex && facets.byIndex[
|
|
1066
|
+
let isKey = !!facets.byIndex && facets.byIndex[TableIndex].all.find((facet) => facet.name === name);
|
|
1012
1067
|
let definition = {
|
|
1013
1068
|
name,
|
|
1014
1069
|
field,
|
|
@@ -1033,6 +1088,7 @@ class Schema {
|
|
|
1033
1088
|
properties: attribute.properties,
|
|
1034
1089
|
parentPath: attribute.parentPath,
|
|
1035
1090
|
parentType: attribute.parentType,
|
|
1091
|
+
padding: attribute.padding,
|
|
1036
1092
|
};
|
|
1037
1093
|
|
|
1038
1094
|
if (definition.type === AttributeTypes.custom) {
|
package/src/service.js
CHANGED
|
@@ -427,7 +427,7 @@ class Service {
|
|
|
427
427
|
return [!!collectionDifferences.length, collectionDifferences];
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
_compareEntityAttributes(definition = {}, providedAttributes = {}) {
|
|
430
|
+
_compareEntityAttributes(entityName, definition = {}, providedAttributes = {}, keys) {
|
|
431
431
|
let results = {
|
|
432
432
|
additions: {},
|
|
433
433
|
invalid: [],
|
|
@@ -438,17 +438,29 @@ class Service {
|
|
|
438
438
|
results.additions[name] = detail;
|
|
439
439
|
} else if (defined.field !== detail.field) {
|
|
440
440
|
results.invalid.push(
|
|
441
|
-
`
|
|
441
|
+
`The attribute "${name}" with Table Field "${detail.field}" does not match established Table Field "${defined.field}"`,
|
|
442
442
|
);
|
|
443
443
|
}
|
|
444
|
+
if (defined && detail && (defined.padding || detail.padding)) {
|
|
445
|
+
const definedPadding = defined.padding || {};
|
|
446
|
+
const detailPadding = detail.padding || {};
|
|
447
|
+
if (keys.pk.facets.includes(name) &&
|
|
448
|
+
(definedPadding.length !== detailPadding.length ||
|
|
449
|
+
definedPadding.char !== detailPadding.char)
|
|
450
|
+
) {
|
|
451
|
+
results.invalid.push(
|
|
452
|
+
`The attribute "${name}" contains inconsistent padding definitions that impact how keys are formed`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
444
456
|
}
|
|
445
457
|
return [!!results.invalid.length, results];
|
|
446
458
|
}
|
|
447
459
|
|
|
448
|
-
_processEntityAttributes(definition = {}, providedAttributes = {}) {
|
|
449
|
-
let [attributesAreIncompatible, attributeResults] = this._compareEntityAttributes(definition, providedAttributes);
|
|
460
|
+
_processEntityAttributes(entityName, definition = {}, providedAttributes = {}, keys) {
|
|
461
|
+
let [attributesAreIncompatible, attributeResults] = this._compareEntityAttributes(entityName, definition, providedAttributes, keys);
|
|
450
462
|
if (attributesAreIncompatible) {
|
|
451
|
-
throw new e.ElectroError(e.ErrorCodes.InvalidJoin, `
|
|
463
|
+
throw new e.ElectroError(e.ErrorCodes.InvalidJoin, `Inconsistent attribute(s) on the entity "${entityName}". The following attribute(s) are defined with incompatible or conflicting definitions across participating entities: ${attributeResults.invalid.join(", ")}. These attribute definitions must match among all members of the collection.`);
|
|
452
464
|
} else {
|
|
453
465
|
return {
|
|
454
466
|
...definition,
|
|
@@ -566,7 +578,7 @@ class Service {
|
|
|
566
578
|
this.collectionSchema[collection].table = entity._getTableName();
|
|
567
579
|
}
|
|
568
580
|
this.collectionSchema[collection].keys = this._processEntityKeys(name, this.collectionSchema[collection].keys, providedIndex);
|
|
569
|
-
this.collectionSchema[collection].attributes = this._processEntityAttributes(this.collectionSchema[collection].attributes, entity.model.schema.attributes);
|
|
581
|
+
this.collectionSchema[collection].attributes = this._processEntityAttributes(name, this.collectionSchema[collection].attributes, entity.model.schema.attributes, this.collectionSchema[collection].keys);
|
|
570
582
|
this.collectionSchema[collection].entities[name] = entity;
|
|
571
583
|
this.collectionSchema[collection].identifiers = this._processEntityIdentifiers(this.collectionSchema[collection].identifiers, entity.getIdentifierExpressions(name));
|
|
572
584
|
this.collectionSchema[collection].index = this._processEntityCollectionIndex(this.collectionSchema[collection].index, providedIndex.index, name, collection);
|
package/src/util.js
CHANGED
|
@@ -184,9 +184,48 @@ const cursorFormatter = {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function removeFixings({prefix = '', postfix = '', value = ''} = {}) {
|
|
188
|
+
const start = value.toLowerCase().startsWith(prefix.toLowerCase()) ? prefix.length : 0;
|
|
189
|
+
const end = value.length - (value.toLowerCase().endsWith(postfix.toLowerCase()) ? postfix.length : 0);
|
|
190
|
+
|
|
191
|
+
let formatted = '';
|
|
192
|
+
for (let i = start; i < end; i++) {
|
|
193
|
+
formatted += value[i];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return formatted;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function addPadding({padding = {}, value = ''} = {}) {
|
|
200
|
+
return value.padStart(padding.length, padding.char);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removePadding({padding = {}, value = ''} = {}) {
|
|
204
|
+
if (!padding.length || value.length >= padding.length) {
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let formatted = '';
|
|
209
|
+
let useRemaining = false;
|
|
210
|
+
for (let i = 0; i < value.length; i++) {
|
|
211
|
+
const char = value[i];
|
|
212
|
+
if (useRemaining || i >= padding.length) {
|
|
213
|
+
formatted += char;
|
|
214
|
+
} else if (char !== padding.char) {
|
|
215
|
+
formatted += char;
|
|
216
|
+
useRemaining = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return formatted;
|
|
221
|
+
}
|
|
222
|
+
|
|
187
223
|
module.exports = {
|
|
188
224
|
getUnique,
|
|
189
225
|
batchItems,
|
|
226
|
+
addPadding,
|
|
227
|
+
removePadding,
|
|
228
|
+
removeFixings,
|
|
190
229
|
parseJSONPath,
|
|
191
230
|
getInstanceType,
|
|
192
231
|
getModelVersion,
|
package/src/validations.js
CHANGED
|
@@ -64,6 +64,18 @@ const Attribute = {
|
|
|
64
64
|
type: "any",
|
|
65
65
|
format: "isFunction",
|
|
66
66
|
},
|
|
67
|
+
padding: {
|
|
68
|
+
type: "object",
|
|
69
|
+
required: ['length', 'char'],
|
|
70
|
+
properties: {
|
|
71
|
+
length: {
|
|
72
|
+
type: 'number'
|
|
73
|
+
},
|
|
74
|
+
char: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
67
79
|
},
|
|
68
80
|
};
|
|
69
81
|
|