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 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: "*" | string[]
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 | Description
641
- ------------ | :--------------------------------------------------------: | :------: | :-------: | -----------
642
- `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.
643
- `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.
644
- `hidden` | `boolean` | no | all | Flag an attribute as hidden to remove the property from results before they are returned.
645
- `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
646
- `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.
647
- `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.
648
- `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.
649
- `label` | `string` | no | all | Used in index composition to prefix key composite attributes. By default, the `AttributeName` is used as the label.
650
- `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
651
- `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.
652
- `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.
653
- `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.
654
- `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.
655
- `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".
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: Performing a BatchGet will return a response structure unique to BatchGet: a two-dimensional array with the results of the query and any unprocessed records. See the example below._
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
- sort | '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)]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/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
- let {name, label} = labels[i];
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}${supplied[name]}`;
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.format(value);
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
- _makeStructureKey({prefix = "", postfix = "", casing= KeyCasing.none} = {}) {
237
- return (key) => {
238
- let value = key;
239
- if (this.type === AttributeTypes.string && v.isStringHasLength(key)) {
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
- _makeDestructureKey({prefix = "", postfix = "", casing= KeyCasing.none} = {}) {
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
- return key;
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
- return u.formatAttributeCasing(value, casing);
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.InvalidIndexCompositeWithAttributeName,
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[""].all.find((facet) => facet.name === name);
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
- `Attribute provided "${name}" with Table Field "${detail.field}" does not match established Table Field "${defined.field}"`,
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, `Invalid entity attributes. The following attributes have already been defined on this model but with incompatible or conflicting properties: ${attributeResults.invalid.join(", ")}`);
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,
@@ -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