electrodb 3.5.3 → 3.6.1

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/index.js CHANGED
@@ -18,7 +18,9 @@ const {
18
18
  const { createConversions } = require("./src/conversions");
19
19
 
20
20
  const {
21
- ComparisonTypes
21
+ ComparisonTypes,
22
+ EntityIdentifiers,
23
+ EntityIdentifierFields,
22
24
  } = require('./src/types');
23
25
 
24
26
  module.exports = {
@@ -35,4 +37,6 @@ module.exports = {
35
37
  ElectroUserValidationError,
36
38
  ElectroAttributeValidationError,
37
39
  createConversions,
40
+ EntityIdentifiers,
41
+ EntityIdentifierFields,
38
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "3.5.3",
3
+ "version": "3.6.1",
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/clauses.js CHANGED
@@ -1267,6 +1267,7 @@ let clauses = {
1267
1267
  operation: options._isTransaction
1268
1268
  ? MethodTypes.transactWrite
1269
1269
  : undefined,
1270
+ state,
1270
1271
  },
1271
1272
  });
1272
1273
 
package/src/client.js CHANGED
@@ -2,6 +2,7 @@ const lib = require("@aws-sdk/lib-dynamodb");
2
2
  const util = require("@aws-sdk/util-dynamodb");
3
3
  const { isFunction } = require("./validations");
4
4
  const { ElectroError, ErrorCodes } = require("./errors");
5
+ const { EntityIdentifiers } = require("./types");
5
6
  const DocumentClientVersions = {
6
7
  v2: "v2",
7
8
  v3: "v3",
@@ -289,9 +290,14 @@ function normalizeClient(client) {
289
290
  }
290
291
 
291
292
  function normalizeConfig(config = {}) {
293
+ const identifiers = config.identifiers || {};
292
294
  return {
293
295
  ...config,
294
296
  client: normalizeClient(config.client),
297
+ identifiers: {
298
+ entity: identifiers.entity || EntityIdentifiers.entity,
299
+ version: identifiers.version || EntityIdentifiers.version
300
+ }
295
301
  };
296
302
  }
297
303
 
package/src/entity.js CHANGED
@@ -30,9 +30,10 @@ const {
30
30
  CastKeyOptions,
31
31
  ComparisonTypes,
32
32
  DataOptions,
33
+ IndexProjectionOptions,
33
34
  } = require("./types");
34
35
  const { FilterFactory } = require("./filters");
35
- const { FilterOperations } = require("./operations");
36
+ const { FilterOperations, formatExpressionName } = require("./operations");
36
37
  const { WhereFactory } = require("./where");
37
38
  const { clauses, ChainState } = require("./clauses");
38
39
  const { EventManager } = require("./events");
@@ -49,16 +50,16 @@ const ImpactedIndexTypeSource = {
49
50
 
50
51
  class Entity {
51
52
  constructor(model, config = {}) {
52
- config = c.normalizeConfig(config);
53
+ this.config = c.normalizeConfig(config);
54
+ this.identifiers = this.config.identifiers;
55
+ this.client = this.config.client;
53
56
  this.eventManager = new EventManager({
54
- listeners: config.listeners,
57
+ listeners: this.config.listeners,
55
58
  });
56
- this.eventManager.add(config.logger);
59
+ this.eventManager.add(this.config.logger);
57
60
  this._validateModel(model);
58
61
  this.version = EntityVersions.v1;
59
- this.config = config;
60
- this.client = config.client;
61
- this.model = this._parseModel(model, config);
62
+ this.model = this._parseModel(model, this.config);
62
63
  /** start beta/v1 condition **/
63
64
  this.config.table = config.table || model.table;
64
65
  /** end beta/v1 condition **/
@@ -94,24 +95,31 @@ class Entity {
94
95
  ).query(...values);
95
96
  };
96
97
  }
97
-
98
- this.config.identifiers = config.identifiers || {};
99
- this.identifiers = {
100
- entity: this.config.identifiers.entity || "__edb_e__",
101
- version: this.config.identifiers.version || "__edb_v__",
102
- };
103
98
  this._instance = ElectroInstance.entity;
104
99
  this._instanceType = ElectroInstanceTypes.entity;
105
100
  this.schema = model;
106
101
  }
107
102
 
108
103
  get scan() {
109
- return this._makeChain(
104
+ const result = this._makeChain(
110
105
  TableIndex,
111
106
  this._clausesWithFilters,
112
107
  clauses.index,
113
108
  { _isPagination: true },
114
109
  ).scan();
110
+
111
+ for (const accessPattern in this.model.indexes) {
112
+ const index = this.model.indexes[accessPattern].index;
113
+
114
+ result[accessPattern] = this._makeChain(
115
+ index,
116
+ this._clausesWithFilters,
117
+ clauses.index,
118
+ { _isPagination: true },
119
+ ).scan();
120
+ }
121
+
122
+ return result;
115
123
  }
116
124
 
117
125
  setIdentifier(type = "", identifier = "") {
@@ -145,40 +153,38 @@ class Entity {
145
153
  );
146
154
  }
147
155
 
148
- _attributesIncludeKeys(attributes = []) {
156
+ _itemIncludesKeys(item) {
149
157
  let { pk, sk } = this.model.prefixes[TableIndex];
150
- let pkFound = false;
151
- let skFound = false;
152
- for (let i = 0; i < attributes.length; i++) {
153
- const attribute = attributes[i];
154
- if (attribute === sk.field) {
155
- skFound = true;
156
- }
157
- if (attribute === pk.field) {
158
- skFound = true;
159
- }
160
- if (pkFound && skFound) {
161
- return true;
162
- }
163
- }
164
- return false;
158
+ return item[pk.field] !== undefined && (
159
+ sk.field === undefined || item[sk.field] !== undefined
160
+ );
165
161
  }
166
162
 
167
163
  ownsKeys(key = {}) {
164
+ const accessPattern =
165
+ this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
168
166
  let { pk, sk } = this.model.prefixes[TableIndex];
169
167
  let hasSK = this.model.lookup.indexHasSortKeys[TableIndex];
170
168
  const typeofPkProvided = typeof key[pk.field];
171
169
  const pkPrefixMatch =
172
- typeofPkProvided === "string" && key[pk.field].startsWith(pk.prefix);
170
+ typeofPkProvided === "string" &&
171
+ key[pk.field].startsWith(pk.prefix) &&
172
+ (!pk.postfix || key[pk.field].endsWith(pk.postfix));
173
173
  const isNumericPk = typeofPkProvided === "number" && pk.cast === "number";
174
174
  let pkMatch = pkPrefixMatch || isNumericPk;
175
175
  let skMatch = pkMatch && !hasSK;
176
176
  if (pkMatch && hasSK) {
177
177
  const typeofSkProvided = typeof key[sk.field];
178
- const skPrefixMatch =
179
- typeofSkProvided === "string" && key[sk.field].startsWith(sk.prefix);
180
- const isNumericSk = typeofSkProvided === "number" && sk.cast === "number";
181
- skMatch = skPrefixMatch || isNumericSk;
178
+ if (typeofSkProvided === "number") {
179
+ skMatch = sk.cast === "number";
180
+ } else if (typeofSkProvided === "string") {
181
+ // if sk is a string, check prefix and postfix match
182
+ const hasNoPrefixOrStartsWithPrefix = !sk.prefix || key[sk.field].startsWith(sk.prefix);
183
+ const hasNoPostfixOrEndsWithPostfix = !sk.postfix || key[sk.field].endsWith(sk.postfix);
184
+ skMatch =
185
+ hasNoPrefixOrStartsWithPrefix &&
186
+ hasNoPostfixOrEndsWithPostfix;
187
+ }
182
188
  }
183
189
 
184
190
  return (
@@ -652,6 +658,7 @@ class Entity {
652
658
  _isCollectionQuery: false,
653
659
  preserveBatchOrder: true,
654
660
  ignoreOwnership: config._providedIgnoreOwnership,
661
+ attributes: config._providedAttributes,
655
662
  });
656
663
 
657
664
  const unprocessed = [];
@@ -689,7 +696,9 @@ class Entity {
689
696
  let iterations = 0;
690
697
  let count = 0;
691
698
  let hydratedUnprocessed = [];
692
- const shouldHydrate = config.hydrate && method === MethodTypes.query;
699
+ const shouldHydrate =
700
+ config.hydrate &&
701
+ (method === MethodTypes.query || method === MethodTypes.scan);
693
702
  do {
694
703
  let response = await this._exec(
695
704
  method,
@@ -908,6 +917,18 @@ class Entity {
908
917
  }
909
918
  }
910
919
 
920
+ is(item, config) {
921
+ return (
922
+ config.ignoreOwnership &&
923
+ !this._itemIncludesKeys(item)
924
+ ) || (
925
+ (config.ignoreOwnership || config.hydrate) &&
926
+ this.ownsKeys(item)
927
+ ) || (
928
+ this.ownsItem(item)
929
+ );
930
+ }
931
+
911
932
  formatResponse(response, index, config = {}) {
912
933
  let stackTrace;
913
934
  if (!config.originalErr) {
@@ -930,15 +951,7 @@ class Entity {
930
951
  results = response;
931
952
  } else {
932
953
  if (response.Item) {
933
- if (
934
- (config.ignoreOwnership &&
935
- config.attributes &&
936
- config.attributes.length > 0 &&
937
- !this._attributesIncludeKeys(config.attributes)) ||
938
- ((config.ignoreOwnership || config.hydrate) &&
939
- this.ownsKeys(response.Item)) ||
940
- this.ownsItem(response.Item)
941
- ) {
954
+ if (this.is(response.Item, config)) {
942
955
  results = this.model.schema.formatItemForRetrieval(
943
956
  response.Item,
944
957
  config,
@@ -949,15 +962,7 @@ class Entity {
949
962
  } else if (response.Items) {
950
963
  results = [];
951
964
  for (let item of response.Items) {
952
- if (
953
- (config.ignoreOwnership &&
954
- config.attributes &&
955
- config.attributes.length > 0 &&
956
- !this._attributesIncludeKeys(config.attributes)) ||
957
- ((config.ignoreOwnership || config.hydrate) &&
958
- this.ownsKeys(item)) ||
959
- this.ownsItem(item)
960
- ) {
965
+ if (this.is(item, config)) {
961
966
  let record = this.model.schema.formatItemForRetrieval(
962
967
  item,
963
968
  config,
@@ -1640,6 +1645,7 @@ class Entity {
1640
1645
  listeners: [],
1641
1646
  preserveBatchOrder: false,
1642
1647
  attributes: [],
1648
+ _providedAttributes: [],
1643
1649
  terminalOperation: undefined,
1644
1650
  formatCursor: u.cursorFormatter,
1645
1651
  order: undefined,
@@ -1649,6 +1655,19 @@ class Entity {
1649
1655
  _includeOnResponseItem: {},
1650
1656
  };
1651
1657
 
1658
+ // Auto-set ignoreOwnership: true for INCLUDE or KEYS_ONLY indexes
1659
+ if (context.state && context.state.query && context.state.query.index) {
1660
+ const indexName = context.state.query.index;
1661
+ const accessPattern =
1662
+ this.model.translations.indexes.fromIndexToAccessPattern[indexName];
1663
+ if (accessPattern) {
1664
+ const indexDefinition = this.model.indexes[accessPattern];
1665
+ config.ignoreOwnership =
1666
+ indexDefinition.projection === IndexProjectionOptions.keys_only ||
1667
+ (Array.isArray(indexDefinition.projection) && !this.model.indexes[accessPattern].identifiersAreProjected);
1668
+ }
1669
+ }
1670
+
1652
1671
  return provided.filter(Boolean).reduce((config, option) => {
1653
1672
  if (typeof option.order === "string") {
1654
1673
  switch (option.order.toLowerCase()) {
@@ -1727,6 +1746,7 @@ class Entity {
1727
1746
 
1728
1747
  if (Array.isArray(option.attributes)) {
1729
1748
  config.attributes = config.attributes.concat(option.attributes);
1749
+ config._providedAttributes = option.attributes;
1730
1750
  }
1731
1751
 
1732
1752
  if (option.preserveBatchOrder === true) {
@@ -1850,7 +1870,7 @@ class Entity {
1850
1870
  }
1851
1871
  }
1852
1872
 
1853
- if (option.ignoreOwnership) {
1873
+ if (option.ignoreOwnership !== undefined) {
1854
1874
  config.ignoreOwnership = option.ignoreOwnership;
1855
1875
  config._providedIgnoreOwnership = option.ignoreOwnership;
1856
1876
  }
@@ -1875,6 +1895,10 @@ class Entity {
1875
1895
  if (option.hydrate) {
1876
1896
  config.hydrate = true;
1877
1897
  config.ignoreOwnership = true;
1898
+ // if we will hydrate later, we don't want to provide a ProjectionExpression since the attributes
1899
+ // may contain non-projected attributes that the user expects to receive from the main table
1900
+ // after hydration
1901
+ config.attributes = [];
1878
1902
  }
1879
1903
 
1880
1904
  if (validations.isFunction(option.hydrator)) {
@@ -2015,6 +2039,7 @@ class Entity {
2015
2039
  params = this._makeScanParam(
2016
2040
  filter[ExpressionTypes.FilterExpression],
2017
2041
  config,
2042
+ state,
2018
2043
  );
2019
2044
  break;
2020
2045
  /* istanbul ignore next */
@@ -2058,27 +2083,41 @@ class Entity {
2058
2083
  TerminalOperation[config.terminalOperation] === TerminalOperation.go ||
2059
2084
  TerminalOperation[config.terminalOperation] === TerminalOperation.page;
2060
2085
 
2061
- // 1. Take stock of invalid attributes, if there are any this should be considered
2062
- // unintentional and should throw to prevent unintended results
2063
- // 2. Convert all attribute names to their respective "field" names
2064
- const unknownAttributes = [];
2065
- let attributeFields = new Set();
2066
- for (const attributeName of attributes) {
2067
- const fieldName = this.model.schema.getFieldName(attributeName);
2068
- if (typeof fieldName !== "string") {
2069
- unknownAttributes.push(attributeName);
2070
- } else {
2071
- attributeFields.add(fieldName);
2086
+ // Convert all attribute names to their respective "field" names
2087
+ let hasTableIndexPk = false;
2088
+ let hasTableIndexSk = !this.model.translations.keys[TableIndex].sk;
2089
+ const attributeFields = new Map();
2090
+ for (let [key, name] of Object.entries(parameters.ExpressionAttributeNames || {})) {
2091
+ if (key.startsWith("#")) {
2092
+ key = key.slice(1);
2093
+ }
2094
+ attributeFields.set(key, name);
2095
+ if (name === this.model.translations.keys[TableIndex].pk) {
2096
+ hasTableIndexPk = true;
2072
2097
  }
2098
+ if (name === this.model.translations.keys[TableIndex].sk) {
2099
+ hasTableIndexSk = true;
2100
+ }
2101
+ }
2102
+
2103
+ if (!hasTableIndexPk) {
2104
+ const field = this.model.translations.keys[TableIndex].pk;
2105
+ const name = formatExpressionName(field, attributeFields);
2106
+ attributeFields.set(name, field);
2073
2107
  }
2074
2108
 
2075
- // Stop doing work, prepare error message and throw
2076
- if (attributeFields.size === 0 || unknownAttributes.length > 0) {
2077
- let message = "Unknown attributes provided in query options";
2078
- if (unknownAttributes.length) {
2079
- message += `: ${u.commaSeparatedString(unknownAttributes)}`;
2109
+ if (!hasTableIndexSk) {
2110
+ const field = this.model.translations.keys[TableIndex].sk;
2111
+ const name = formatExpressionName(field, attributeFields);
2112
+ attributeFields.set(name, field);
2113
+ }
2114
+
2115
+ for (const attributeName of attributes) {
2116
+ const fieldName = this.model.schema.getFieldName(attributeName) || attributeName;
2117
+ if (fieldName) {
2118
+ const formatted = formatExpressionName(fieldName, attributeFields);
2119
+ attributeFields.set(formatted, fieldName);
2080
2120
  }
2081
- throw new e.ElectroError(e.ErrorCodes.InvalidOptions, message);
2082
2121
  }
2083
2122
 
2084
2123
  // add ExpressionAttributeNames if it doesn't exist already
@@ -2095,8 +2134,8 @@ class Entity {
2095
2134
  enforcesOwnership
2096
2135
  ) {
2097
2136
  // add entity identifiers to so items can be identified
2098
- attributeFields.add(this.identifiers.entity);
2099
- attributeFields.add(this.identifiers.version);
2137
+ attributeFields.set(this.identifiers.entity, this.identifiers.entity);
2138
+ attributeFields.set(this.identifiers.version, this.identifiers.version);
2100
2139
 
2101
2140
  // if pagination is required you may enter into a scenario where
2102
2141
  // the LastEvaluatedKey doesn't belong to entity and one must be formed.
@@ -2111,22 +2150,23 @@ class Entity {
2111
2150
 
2112
2151
  for (const attribute of [...tableIndexFacets.all, ...indexFacets.all]) {
2113
2152
  const fieldName = this.model.schema.getFieldName(attribute.name);
2114
- attributeFields.add(fieldName);
2153
+ const formatted = formatExpressionName(attribute.name, attributeFields);
2154
+ attributeFields.set(formatted, fieldName);
2115
2155
  }
2116
2156
  }
2117
2157
  }
2118
2158
 
2119
- for (const attributeField of attributeFields) {
2159
+ for (const [attributeField, attributeName] of attributeFields.entries()) {
2120
2160
  // prefix the ExpressionAttributeNames because some prefixes are not allowed
2121
2161
  parameters.ExpressionAttributeNames["#" + attributeField] =
2122
- attributeField;
2162
+ attributeName;
2123
2163
  }
2124
2164
 
2125
2165
  // if there is already a ProjectionExpression (e.g. config "params"), merge it
2126
2166
  if (typeof parameters.ProjectionExpression === "string") {
2127
2167
  parameters.ProjectionExpression = [
2128
2168
  parameters.ProjectionExpression,
2129
- ...Object.keys([parameters.ExpressionAttributeNames]),
2169
+ ...Object.keys(parameters.ExpressionAttributeNames),
2130
2170
  ].join(", ");
2131
2171
  } else {
2132
2172
  parameters.ProjectionExpression = Object.keys(
@@ -2234,17 +2274,17 @@ class Entity {
2234
2274
  }
2235
2275
 
2236
2276
  /* istanbul ignore next */
2237
- _makeScanParam(filter = {}, options = {}) {
2238
- let indexBase = TableIndex;
2239
- let hasSortKey = this.model.lookup.indexHasSortKeys[indexBase];
2277
+ _makeScanParam(filter = {}, options = {}, state = {}) {
2278
+ let index = state.query.index || TableIndex;
2279
+ let hasSortKey = this.model.lookup.indexHasSortKeys[index];
2240
2280
  let accessPattern =
2241
- this.model.translations.indexes.fromIndexToAccessPattern[indexBase];
2281
+ this.model.translations.indexes.fromIndexToAccessPattern[index];
2242
2282
  let pkField = this.model.indexes[accessPattern].pk.field;
2243
2283
  let { pk, sk } = this._makeIndexKeys({
2244
- index: indexBase,
2284
+ index,
2245
2285
  });
2246
2286
 
2247
- let keys = this._makeParameterKey(indexBase, pk, ...sk);
2287
+ let keys = this._makeParameterKey(index, pk, ...sk);
2248
2288
  // trim empty key values (this can occur when keys are defined by users)
2249
2289
  for (let key in keys) {
2250
2290
  if (keys[key] === undefined || keys[key] === "") {
@@ -2297,6 +2337,10 @@ class Entity {
2297
2337
  params.FilterExpression = filterExpressions.join(" AND ");
2298
2338
  }
2299
2339
 
2340
+ if (index) {
2341
+ params.IndexName = index;
2342
+ }
2343
+
2300
2344
  return this._applyProjectionExpressions({
2301
2345
  parameters: params,
2302
2346
  config: options,
@@ -4227,7 +4271,7 @@ class Entity {
4227
4271
  return model;
4228
4272
  }
4229
4273
 
4230
- _normalizeIndexes(indexes) {
4274
+ _normalizeIndexes(indexes, config) {
4231
4275
  let normalized = {};
4232
4276
  let indexFieldTranslation = {};
4233
4277
  let indexHasSortKeys = {};
@@ -4253,6 +4297,7 @@ class Entity {
4253
4297
  fields: [],
4254
4298
  attributes: [],
4255
4299
  labels: {},
4300
+ projections: [],
4256
4301
  };
4257
4302
  let seenIndexes = {};
4258
4303
  let seenIndexFields = {};
@@ -4370,7 +4415,7 @@ class Entity {
4370
4415
  e.ErrorCodes.DuplicateIndexCompositeAttributes,
4371
4416
  `The Access Pattern '${accessPattern}' contains duplicate references the composite attribute(s): ${u.commaSeparatedString(
4372
4417
  duplicates,
4373
- )}. Composite attributes can only be used more than once in an index if your sort key is limitted to a single attribute. This is to prevent unexpected runtime errors related to the inability to generate keys.`,
4418
+ )}. Composite attributes can only be used more than once in an index if your sort key is limited to a single attribute. This is to prevent unexpected runtime errors related to the inability to generate keys.`,
4374
4419
  );
4375
4420
  }
4376
4421
  }
@@ -4387,8 +4432,39 @@ class Entity {
4387
4432
  scope: indexScope,
4388
4433
  condition: indexCondition,
4389
4434
  conditionDefined: conditionDefined,
4435
+ projection: index.projection,
4436
+ identifiersAreProjected: false,
4390
4437
  };
4391
4438
 
4439
+ let projections = [];
4440
+
4441
+ if (index.projection !== undefined) {
4442
+ if (typeof index.projection === "string" && (
4443
+ index.projection.toLowerCase() === IndexProjectionOptions.keys_only ||
4444
+ index.projection.toLowerCase() === IndexProjectionOptions.all
4445
+ )) {
4446
+ definition.projection = index.projection.toLowerCase();
4447
+ } else if (Array.isArray(index.projection) && index.projection.length > 0 && index.projection.every((attr) => typeof attr === "string")) {
4448
+ definition.projection = index.projection;
4449
+ let nameIdentifierPresent = false;
4450
+ let versionIdentiferPresent = false;
4451
+ for (const name of definition.projection) {
4452
+ projections.push({ name, accessPattern });
4453
+ if (name === config.identifiers.entity) {
4454
+ nameIdentifierPresent = true;
4455
+ } else if (name === config.identifiers.version) {
4456
+ versionIdentiferPresent = true;
4457
+ }
4458
+ }
4459
+ definition.identifiersAreProjected = nameIdentifierPresent && versionIdentiferPresent;
4460
+ } else {
4461
+ throw new e.ElectroError(
4462
+ e.ErrorCodes.InvalidProjectionDefinition,
4463
+ `The Access Pattern '${accessPattern}' contains an invalid "projection" value: ${u.toDisplayString(index.projection)}. Valid projection values include ${u.commaSeparatedString(Object.values(IndexProjectionOptions))}, or an array of attribute names with a length greater than one.`
4464
+ )
4465
+ }
4466
+ }
4467
+
4392
4468
  indexHasSubCollections[indexName] =
4393
4469
  inCollection && Array.isArray(collection);
4394
4470
 
@@ -4444,6 +4520,7 @@ class Entity {
4444
4520
  };
4445
4521
 
4446
4522
  facets.attributes = [...facets.attributes, ...attributes];
4523
+ facets.projections = [...facets.projections, ...projections];
4447
4524
 
4448
4525
  facets.fields.push(pk.field);
4449
4526
 
@@ -4844,10 +4921,14 @@ class Entity {
4844
4921
  indexHasSortKeys,
4845
4922
  indexAccessPattern,
4846
4923
  indexHasSubCollections,
4847
- } = this._normalizeIndexes(model.indexes);
4924
+ } = this._normalizeIndexes(model.indexes, config);
4848
4925
  let schema = new Schema(model.attributes, facets, {
4849
- getClient: () => this.client,
4850
4926
  isRoot: true,
4927
+ getClient: () => this.client,
4928
+ identifiers: {
4929
+ [config.identifiers.entity]: config.identifiers.entity,
4930
+ [config.identifiers.version]: config.identifiers.version,
4931
+ },
4851
4932
  });
4852
4933
 
4853
4934
  let filters = this._normalizeFilters(model.filters);
@@ -4951,6 +5032,7 @@ function matchToEntityAlias({
4951
5032
  record,
4952
5033
  entities = {},
4953
5034
  allowMatchOnKeys = false,
5035
+ config = {},
4954
5036
  } = {}) {
4955
5037
  let entity;
4956
5038
  if (paramItem && v.isFunction(paramItem[TransactionCommitSymbol])) {
@@ -4984,7 +5066,7 @@ function matchToEntityAlias({
4984
5066
  // entityAlias = alias;
4985
5067
  // }
4986
5068
  // }
4987
- } else if (entities[alias] && entities[alias].ownsKeys(record)) {
5069
+ } else if (entities[alias] && entities[alias].is(record, config)) {
4988
5070
  entityAlias = alias;
4989
5071
  break;
4990
5072
  }
package/src/errors.js CHANGED
@@ -133,18 +133,6 @@ const ErrorCodes = {
133
133
  name: "IncompatibleKeyCasing",
134
134
  sym: ErrorCode,
135
135
  },
136
- InvalidListenerProvided: {
137
- code: 1020,
138
- section: "invalid-listener-provided",
139
- name: "InvalidListenerProvided",
140
- sym: ErrorCode,
141
- },
142
- InvalidLoggerProvided: {
143
- code: 1020,
144
- section: "invalid-listener-provided",
145
- name: "InvalidListenerProvided",
146
- sym: ErrorCode,
147
- },
148
136
  InvalidClientProvided: {
149
137
  code: 1021,
150
138
  section: "invalid-client-provided",
@@ -157,6 +145,24 @@ const ErrorCodes = {
157
145
  name: "Inconsistent Index Definition",
158
146
  sym: ErrorCode,
159
147
  },
148
+ InvalidListenerProvided: {
149
+ code: 1023,
150
+ section: "invalid-listener-provided",
151
+ name: "InvalidListenerProvided",
152
+ sym: ErrorCode,
153
+ },
154
+ InvalidLoggerProvided: {
155
+ code: 1024,
156
+ section: "invalid-listener-provided",
157
+ name: "InvalidListenerProvided",
158
+ sym: ErrorCode,
159
+ },
160
+ InvalidProjectionDefinition: {
161
+ code: 1025,
162
+ section: "invalid-projection-definition",
163
+ name: "InvalidProjectionDefinition",
164
+ sym: ErrorCode,
165
+ },
160
166
  MissingAttribute: {
161
167
  code: 2001,
162
168
  section: "missing-attribute",
@@ -292,6 +298,7 @@ class ElectroError extends Error {
292
298
  if (code && code.sym === ErrorCode) {
293
299
  detail = code;
294
300
  }
301
+ this.cause = cause;
295
302
  this._message = message;
296
303
  // this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`;
297
304
  this.message = makeMessage(message, detail.section);
package/src/operations.js CHANGED
@@ -9,6 +9,37 @@ const { FilterOperations } = require("./filterOperations");
9
9
  const e = require("./errors");
10
10
  const u = require("./util");
11
11
 
12
+
13
+ /**
14
+ * formatExpressionName: formats a field name for expression attribute names parameters.
15
+ * @param {string} name
16
+ * @param {Map<string, string>} seen
17
+ */
18
+ function formatExpressionName(name, seen) {
19
+ const nameWasNotANumber = isNaN(name);
20
+ const originalName = `${name}`;
21
+ let formattedName = originalName.replaceAll(/[^\w]/g, "");
22
+
23
+ if (formattedName.length === 0) {
24
+ formattedName = "p";
25
+ } else if (nameWasNotANumber !== isNaN(formattedName)) {
26
+ // name became number due to replace
27
+ formattedName = `p${formattedName}`;
28
+ }
29
+
30
+ const originalFormattedName = formattedName;
31
+ let nameSuffix = 1;
32
+ while (
33
+ seen.has(formattedName) &&
34
+ seen.get(formattedName) !== originalName
35
+ ) {
36
+ formattedName = `${originalFormattedName}_${++nameSuffix}`;
37
+ }
38
+
39
+ seen.set(formattedName, originalName);
40
+ return formattedName;
41
+ }
42
+
12
43
  class ExpressionState {
13
44
  constructor({ prefix } = {}) {
14
45
  this.names = {};
@@ -30,29 +61,7 @@ class ExpressionState {
30
61
  }
31
62
 
32
63
  formatName(name = "") {
33
- const nameWasNotANumber = isNaN(name);
34
- const originalName = `${name}`;
35
- let formattedName = originalName.replaceAll(/[^\w]/g, "");
36
-
37
- if (formattedName.length === 0) {
38
- formattedName = "p";
39
- } else if (nameWasNotANumber !== isNaN(formattedName)) {
40
- // name became number due to replace
41
- formattedName = `p${formattedName}`;
42
- }
43
-
44
- const originalFormattedName = formattedName;
45
- let nameSuffix = 1;
46
-
47
- while (
48
- this.formattedNameToOriginalNameMap.has(formattedName) &&
49
- this.formattedNameToOriginalNameMap.get(formattedName) !== originalName
50
- ) {
51
- formattedName = `${originalFormattedName}_${++nameSuffix}`;
52
- }
53
-
54
- this.formattedNameToOriginalNameMap.set(formattedName, originalName);
55
- return formattedName;
64
+ return formatExpressionName(name, this.formattedNameToOriginalNameMap);
56
65
  }
57
66
 
58
67
  // todo: make the structure: name, value, paths
@@ -403,4 +412,5 @@ module.exports = {
403
412
  FilterOperationNames,
404
413
  ExpressionState,
405
414
  AttributeOperationProxy,
415
+ formatExpressionName,
406
416
  };