@karmaniverous/entity-manager 2.1.0 → 2.1.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/README.md CHANGED
@@ -221,10 +221,10 @@ This change can be accomplished with no breaking changes to existing implementat
221
221
  * [new exports.EntityManager(options)](#new_module_entity-manager.EntityManager_new)
222
222
  * [.addKeys(entityToken, item, [overwrite])](#module_entity-manager.EntityManager+addKeys) ⇒ <code>object</code>
223
223
  * [.calcShardKey(entityToken, item)](#module_entity-manager.EntityManager+calcShardKey) ⇒ <code>string</code>
224
- * [.dehydrateIndex(entityToken, indexToken, item, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
225
- * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
224
+ * [.dehydrateIndex(entityToken, indexToken, index, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
226
225
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
227
226
  * [.query(entityToken, keyToken, item, shardQuery, [options])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
227
+ * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
228
228
  * [.removeKeys(entityToken, item)](#module_entity-manager.EntityManager+removeKeys) ⇒ <code>object</code>
229
229
  * _inner_
230
230
  * [~ShardQueryResult](#module_entity-manager..ShardQueryResult) : <code>object</code>
@@ -242,10 +242,10 @@ Manage DynamoDb entities.
242
242
  * [new exports.EntityManager(options)](#new_module_entity-manager.EntityManager_new)
243
243
  * [.addKeys(entityToken, item, [overwrite])](#module_entity-manager.EntityManager+addKeys) ⇒ <code>object</code>
244
244
  * [.calcShardKey(entityToken, item)](#module_entity-manager.EntityManager+calcShardKey) ⇒ <code>string</code>
245
- * [.dehydrateIndex(entityToken, indexToken, item, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
246
- * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
245
+ * [.dehydrateIndex(entityToken, indexToken, index, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
247
246
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
248
247
  * [.query(entityToken, keyToken, item, shardQuery, [options])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
248
+ * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
249
249
  * [.removeKeys(entityToken, item)](#module_entity-manager.EntityManager+removeKeys) ⇒ <code>object</code>
250
250
 
251
251
  <a name="new_module_entity-manager.EntityManager_new"></a>
@@ -300,8 +300,8 @@ Calculated the shard key for an entity item.
300
300
 
301
301
  <a name="module_entity-manager.EntityManager+dehydrateIndex"></a>
302
302
 
303
- #### entityManager.dehydrateIndex(entityToken, indexToken, item, [delimiter]) ⇒ <code>string</code>
304
- Convert a item into a delimited string containing the properties of a named index.
303
+ #### entityManager.dehydrateIndex(entityToken, indexToken, index, [delimiter]) ⇒ <code>string</code>
304
+ Condense an index object into a delimited string.
305
305
 
306
306
  **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
307
307
  **Returns**: <code>string</code> - Dehydrated index.
@@ -316,27 +316,7 @@ Convert a item into a delimited string containing the properties of a named inde
316
316
  | --- | --- | --- | --- |
317
317
  | entityToken | <code>string</code> | | Entity token. |
318
318
  | indexToken | <code>string</code> | | Index token. |
319
- | item | <code>object</code> | | Entity item. |
320
- | [delimiter] | <code>string</code> | <code>&quot;~&quot;</code> | Delimiter. |
321
-
322
- <a name="module_entity-manager.EntityManager+rehydrateIndex"></a>
323
-
324
- #### entityManager.rehydrateIndex(entityToken, indexToken, value, [delimiter]) ⇒ <code>object</code>
325
- Convert a delimited string into a named index key.
326
-
327
- **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
328
- **Returns**: <code>object</code> - Rehydrated index key.
329
- **Throws**:
330
-
331
- - <code>Error</code> If entityToken is invalid.
332
- - <code>Error</code> If indexToken is invalid.
333
-
334
-
335
- | Param | Type | Default | Description |
336
- | --- | --- | --- | --- |
337
- | entityToken | <code>string</code> | | Entity token. |
338
- | indexToken | <code>string</code> | | Index token. |
339
- | value | <code>string</code> | | Dehydrated index value. |
319
+ | index | <code>object</code> | | Index object. |
340
320
  | [delimiter] | <code>string</code> | <code>&quot;~&quot;</code> | Delimiter. |
341
321
 
342
322
  <a name="module_entity-manager.EntityManager+getKeySpace"></a>
@@ -379,6 +359,26 @@ Query an entity across shards.
379
359
  | [options.limit] | <code>number</code> | Request limit. |
380
360
  | [options.pageKeys] | <code>object</code> | Map of shard page keys. |
381
361
 
362
+ <a name="module_entity-manager.EntityManager+rehydrateIndex"></a>
363
+
364
+ #### entityManager.rehydrateIndex(entityToken, indexToken, value, [delimiter]) ⇒ <code>object</code>
365
+ Convert a delimited string into a named index key.
366
+
367
+ **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
368
+ **Returns**: <code>object</code> - Rehydrated index key.
369
+ **Throws**:
370
+
371
+ - <code>Error</code> If entityToken is invalid.
372
+ - <code>Error</code> If indexToken is invalid.
373
+
374
+
375
+ | Param | Type | Default | Description |
376
+ | --- | --- | --- | --- |
377
+ | entityToken | <code>string</code> | | Entity token. |
378
+ | indexToken | <code>string</code> | | Index token. |
379
+ | value | <code>string</code> | | Dehydrated index value. |
380
+ | [delimiter] | <code>string</code> | <code>&quot;~&quot;</code> | Delimiter. |
381
+
382
382
  <a name="module_entity-manager.EntityManager+removeKeys"></a>
383
383
 
384
384
  #### entityManager.removeKeys(entityToken, item) ⇒ <code>object</code>
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.EntityManager = void 0;
7
+ var _zipObject2 = _interopRequireDefault(require("lodash/zipObject"));
7
8
  var _map2 = _interopRequireDefault(require("lodash/map"));
8
9
  var _size2 = _interopRequireDefault(require("lodash/size"));
9
10
  var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty"));
@@ -11,14 +12,11 @@ var _fromPairs2 = _interopRequireDefault(require("lodash/fromPairs"));
11
12
  var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
12
13
  var _isInteger2 = _interopRequireDefault(require("lodash/isInteger"));
13
14
  var _isUndefined2 = _interopRequireDefault(require("lodash/isUndefined"));
14
- var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
15
15
  var _sortedUniq2 = _interopRequireDefault(require("lodash/sortedUniq"));
16
- var _zipObject2 = _interopRequireDefault(require("lodash/zipObject"));
17
- var _flatten2 = _interopRequireDefault(require("lodash/flatten"));
18
- var _uniq2 = _interopRequireDefault(require("lodash/uniq"));
16
+ var _entries2 = _interopRequireDefault(require("lodash/entries"));
19
17
  var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
20
- var _pick2 = _interopRequireDefault(require("lodash/pick"));
21
- var _values2 = _interopRequireDefault(require("lodash/values"));
18
+ var _sortedUniqBy2 = _interopRequireDefault(require("lodash/sortedUniqBy"));
19
+ var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
22
20
  var _forEach2 = _interopRequireDefault(require("lodash/forEach"));
23
21
  var _isNil2 = _interopRequireDefault(require("lodash/isNil"));
24
22
  var _cloneDeep2 = _interopRequireDefault(require("lodash/cloneDeep"));
@@ -33,7 +31,6 @@ function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _
33
31
  function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
34
32
  function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } }
35
33
  var _entityManager = /*#__PURE__*/new WeakMap();
36
- var _getIndexProperties = /*#__PURE__*/new WeakMap();
37
34
  /**
38
35
  * Manage DynamoDb entities.
39
36
  *
@@ -59,10 +56,6 @@ class EntityManager {
59
56
  writable: true,
60
57
  value: void 0
61
58
  });
62
- _classPrivateFieldInitSpec(this, _getIndexProperties, {
63
- writable: true,
64
- value: (entityToken, indexComponents) => (0, _sortBy2.default)((0, _uniq2.default)((0, _flatten2.default)(indexComponents.map(keyToken => (0, _inspectParametersDeclaration.getParametersNames)(_classPrivateFieldGet(this, _entityManager).getKeyGenerator(entityToken, keyToken))))))
65
- });
66
59
  _classPrivateFieldSet(this, _entityManager, new _PrivateEntityManager.PrivateEntityManager({
67
60
  config,
68
61
  logger
@@ -103,8 +96,11 @@ class EntityManager {
103
96
  }
104
97
 
105
98
  // Add keys.
106
- (0, _forEach2.default)(keys, (getValue, key) => {
107
- if (overwrite || (0, _isNil2.default)(newItem[key])) newItem[key] = getValue(newItem);
99
+ (0, _forEach2.default)(keys, (_ref2, key) => {
100
+ let {
101
+ encode
102
+ } = _ref2;
103
+ if (overwrite || (0, _isNil2.default)(newItem[key])) newItem[key] = encode(newItem);
108
104
  });
109
105
  _classPrivateFieldGet(this, _entityManager).logger.debug('done', newItem);
110
106
 
@@ -130,52 +126,42 @@ class EntityManager {
130
126
  }
131
127
 
132
128
  /**
133
- * Convert a item into a delimited string containing the properties of a named index.
129
+ * Condense an index object into a delimited string.
134
130
  *
135
131
  * @param {string} entityToken - Entity token.
136
132
  * @param {string} indexToken - Index token.
137
- * @param {object} item - Entity item.
133
+ * @param {object} index - Index object.
138
134
  * @param {string} [delimiter] - Delimiter.
139
135
  * @returns {string} Dehydrated index.
140
136
  * @throws {Error} If entityToken is invalid.
141
137
  * @throws {Error} If indexToken is invalid.
142
138
  * @throws {Error} If item is invalid.
143
139
  */
144
- dehydrateIndex(entityToken, indexToken, item) {
140
+ dehydrateIndex(entityToken, indexToken, index) {
145
141
  let delimiter = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '~';
146
142
  // Validate item.
147
- _classPrivateFieldGet(this, _entityManager).validateItem(item);
143
+ _classPrivateFieldGet(this, _entityManager).validateItem(index);
148
144
  const indexComponents = _classPrivateFieldGet(this, _entityManager).getIndexComponents(entityToken, indexToken);
149
- const indexProperties = _classPrivateFieldGet(this, _getIndexProperties).call(this, entityToken, indexComponents);
150
- const {
151
- shardKeyToken
152
- } = _classPrivateFieldGet(this, _entityManager);
153
- return (0, _values2.default)((0, _pick2.default)({
154
- ...item,
155
- [shardKeyToken]: this.calcShardKey(entityToken, item)
156
- }, indexProperties)).join(delimiter);
157
- }
158
- /**
159
- * Convert a delimited string into a named index key.
160
- *
161
- * @param {string} entityToken - Entity token.
162
- * @param {string} indexToken - Index token.
163
- * @param {string} value - Dehydrated index value.
164
- * @param {string} [delimiter] - Delimiter.
165
- * @returns {object} Rehydrated index key.
166
- * @throws {Error} If entityToken is invalid.
167
- * @throws {Error} If indexToken is invalid.
168
- */
169
- rehydrateIndex(entityToken, indexToken) {
170
- let value = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
171
- let delimiter = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '~';
172
- const indexComponents = _classPrivateFieldGet(this, _entityManager).getIndexComponents(entityToken, indexToken);
173
- const indexProperties = _classPrivateFieldGet(this, _getIndexProperties).call(this, entityToken, indexComponents);
174
- const item = (0, _zipObject2.default)(indexProperties, value.split(delimiter));
175
- return indexComponents.reduce((index, component) => ({
176
- ...index,
177
- [component]: _classPrivateFieldGet(this, _entityManager).getKeyGenerator(entityToken, component)(item)
178
- }), {});
145
+ const indexProperties = indexComponents.reduce((properties, component) => {
146
+ const {
147
+ decode
148
+ } = _classPrivateFieldGet(this, _entityManager).getKey(entityToken, component);
149
+ if (!(0, _isFunction2.default)(decode)) throw new Error(`no decode function defined on ${entityToken} entity key '${component}'`);
150
+ return {
151
+ ...properties,
152
+ ...decode(index[component])
153
+ };
154
+ }, {});
155
+ return (0, _sortedUniqBy2.default)((0, _sortBy2.default)((0, _entries2.default)(indexProperties), _ref3 => {
156
+ let [key] = _ref3;
157
+ return key;
158
+ }), _ref4 => {
159
+ let [key] = _ref4;
160
+ return key;
161
+ }).map(_ref5 => {
162
+ let [, value] = _ref5;
163
+ return value;
164
+ }).join(delimiter);
179
165
  }
180
166
 
181
167
  /**
@@ -195,7 +181,7 @@ class EntityManager {
195
181
  let timestamp = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : Date.now();
196
182
  _classPrivateFieldGet(this, _entityManager).logger.debug(`getting shard key space for ${entityToken} on key '${keyToken}' at timestamp ${timestamp}...`, item);
197
183
  const shardKeySpace = _classPrivateFieldGet(this, _entityManager).getShardKeySpace(entityToken, timestamp);
198
- const result = (0, _sortedUniq2.default)(shardKeySpace.map(shardKey => _classPrivateFieldGet(this, _entityManager).getKeyGenerator(entityToken, keyToken)({
184
+ const result = (0, _sortedUniq2.default)(shardKeySpace.map(shardKey => _classPrivateFieldGet(this, _entityManager).getKey(entityToken, keyToken).encode({
199
185
  ...item,
200
186
  [_classPrivateFieldGet(this, _entityManager).shardKeyToken]: shardKey
201
187
  })));
@@ -266,11 +252,11 @@ class EntityManager {
266
252
  const shardQueryResults = await Promise.all((0, _map2.default)(pageKeys, (pageKey, shardedKey) => new Promise(resolve => shardQuery(shardedKey, {
267
253
  pageKey,
268
254
  limit: shardLimit
269
- }).then(_ref2 => {
255
+ }).then(_ref6 => {
270
256
  let {
271
257
  items,
272
258
  pageKey
273
- } = _ref2;
259
+ } = _ref6;
274
260
  return resolve({
275
261
  shardedKey,
276
262
  items,
@@ -279,12 +265,12 @@ class EntityManager {
279
265
  }))));
280
266
 
281
267
  // Reduce shardQueryResults into a single result.
282
- const result = shardQueryResults.reduce((shardedQueryResult, _ref3) => {
268
+ const result = shardQueryResults.reduce((shardedQueryResult, _ref7) => {
283
269
  let {
284
270
  shardedKey,
285
271
  items,
286
272
  pageKey
287
- } = _ref3;
273
+ } = _ref7;
288
274
  return {
289
275
  items: [...shardedQueryResult.items, ...items],
290
276
  pageKeys: {
@@ -301,6 +287,39 @@ class EntityManager {
301
287
  return result;
302
288
  }
303
289
 
290
+ /**
291
+ * Convert a delimited string into a named index key.
292
+ *
293
+ * @param {string} entityToken - Entity token.
294
+ * @param {string} indexToken - Index token.
295
+ * @param {string} value - Dehydrated index value.
296
+ * @param {string} [delimiter] - Delimiter.
297
+ * @returns {object} Rehydrated index key.
298
+ * @throws {Error} If entityToken is invalid.
299
+ * @throws {Error} If indexToken is invalid.
300
+ */
301
+ rehydrateIndex(entityToken, indexToken) {
302
+ let value = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
303
+ let delimiter = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '~';
304
+ const indexComponents = _classPrivateFieldGet(this, _entityManager).getIndexComponents(entityToken, indexToken);
305
+ const indexKeys = (0, _sortBy2.default)(indexComponents.reduce((keys, component) => {
306
+ const {
307
+ encode
308
+ } = _classPrivateFieldGet(this, _entityManager).getKey(entityToken, component);
309
+ return [...new Set([...keys, ...(0, _inspectParametersDeclaration.getParametersNames)(encode)]).values()];
310
+ }, []));
311
+ const indexProperies = (0, _zipObject2.default)(indexKeys, value.split(delimiter));
312
+ return indexComponents.reduce((index, component) => {
313
+ const {
314
+ encode
315
+ } = _classPrivateFieldGet(this, _entityManager).getKey(entityToken, component);
316
+ return {
317
+ ...index,
318
+ [component]: encode(indexProperies)
319
+ };
320
+ }, {});
321
+ }
322
+
304
323
  /**
305
324
  * Remove sharded keys from an entity item. Does not mutate original item.
306
325
  *
@@ -12,8 +12,10 @@ var _filter2 = _interopRequireDefault(require("lodash/filter"));
12
12
  var _findLast2 = _interopRequireDefault(require("lodash/findLast"));
13
13
  var _has2 = _interopRequireDefault(require("lodash/has"));
14
14
  var _map2 = _interopRequireDefault(require("lodash/map"));
15
- var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
16
15
  var _isNil2 = _interopRequireDefault(require("lodash/isNil"));
16
+ var _isUndefined2 = _interopRequireDefault(require("lodash/isUndefined"));
17
+ var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
18
+ var _forEach2 = _interopRequireDefault(require("lodash/forEach"));
17
19
  var _keys2 = _interopRequireDefault(require("lodash/keys"));
18
20
  var _difference2 = _interopRequireDefault(require("lodash/difference"));
19
21
  var _some2 = _interopRequireDefault(require("lodash/some"));
@@ -102,6 +104,15 @@ const configSchema = {
102
104
  },
103
105
  additionalProperties: false
104
106
  };
107
+ const keySchema = {
108
+ type: 'object',
109
+ properties: {
110
+ encode: {},
111
+ decode: {}
112
+ },
113
+ additionalProperties: false,
114
+ required: ['encode']
115
+ };
105
116
 
106
117
  /**
107
118
  * Private EntityManager implementation.
@@ -197,7 +208,7 @@ class PrivateEntityManager {
197
208
  shardKeyToken
198
209
  };
199
210
 
200
- // Validate entity properties.
211
+ // Conform & validate entity properties.
201
212
  (0, _some2.default)(conformedConfig.entities, (_ref2, entityToken) => {
202
213
  let {
203
214
  indexes,
@@ -220,13 +231,26 @@ class PrivateEntityManager {
220
231
  } else return false;
221
232
  });
222
233
 
223
- // Validate entity keys are functions or undefined.
224
- (0, _some2.default)(keys, (value, key) => {
225
- if (!(0, _isNil2.default)(value) && !(0, _isFunction2.default)(value)) {
226
- const message = `${entityToken} key '${key}' must be a function or nil.`;
234
+ // Conform & validate entity keys.
235
+ (0, _forEach2.default)(keys, (value, key) => {
236
+ keys[key] = (0, _isFunction2.default)(value) ? {
237
+ encode: value
238
+ } : value;
239
+
240
+ // Validate key against schema.
241
+ const validatorResult = (0, _jsonschema.validate)(keys[key], keySchema);
242
+ if (!validatorResult.valid) {
243
+ validatorResult.errors.forEach(error => this.logger.error(error.message));
244
+ throw new Error(validatorResult.errors);
245
+ }
246
+ const {
247
+ decode
248
+ } = keys[key];
249
+ if (!(0, _isUndefined2.default)(decode) && !(0, _isFunction2.default)(decode)) {
250
+ const message = `entity ${entityKey} key '${key}' decode must be a function or undefined.`;
227
251
  this.logger.error(message);
228
252
  throw new Error(message);
229
- } else return false;
253
+ }
230
254
  });
231
255
 
232
256
  // Validate sharding bump values increase monotonically with keys.
@@ -320,7 +344,7 @@ class PrivateEntityManager {
320
344
  }
321
345
  return indexes[indexToken];
322
346
  }
323
- getKeyGenerator(entityToken, keyToken) {
347
+ getKey(entityToken, keyToken) {
324
348
  const {
325
349
  keys
326
350
  } = this.getEntityConfig(entityToken);
@@ -416,30 +440,30 @@ class PrivateEntityManager {
416
440
  }
417
441
 
418
442
  /**
419
- * Tests whether a key token is valid.
443
+ * Tests whether an item is valid.
420
444
  *
421
- * @param {string} entityToken - Entity token.
422
- * @param {string} keyToken - Key token.
423
- * @returns {boolean} true if keyToken is valid.
445
+ * @param {string} item - Entity token.
446
+ * @returns {boolean} true if entityToken is valid.
424
447
  */
425
- validateKeyToken(entityToken, keyToken) {
426
- this.validateEntityToken(entityToken);
427
- if (!this.config.entities[entityToken].keys[keyToken]) {
428
- const message = `Invalid entity ${entityToken} keyToken: ${keyToken}`;
448
+ validateItem(item) {
449
+ if (!(0, _isPlainObject2.default)(item)) {
450
+ const message = `Invalid item: ${item}`;
429
451
  this.logger.error(message);
430
452
  throw new Error(message);
431
453
  } else return true;
432
454
  }
433
455
 
434
456
  /**
435
- * Tests whether an item is valid.
457
+ * Tests whether a key token is valid.
436
458
  *
437
- * @param {string} item - Entity token.
438
- * @returns {boolean} true if entityToken is valid.
459
+ * @param {string} entityToken - Entity token.
460
+ * @param {string} keyToken - Key token.
461
+ * @returns {boolean} true if keyToken is valid.
439
462
  */
440
- validateItem(item) {
441
- if (!(0, _isPlainObject2.default)(item)) {
442
- const message = `Invalid item: ${item}`;
463
+ validateKeyToken(entityToken, keyToken) {
464
+ this.validateEntityToken(entityToken);
465
+ if (!this.config.entities[entityToken].keys[keyToken]) {
466
+ const message = `Invalid entity ${entityToken} keyToken: ${keyToken}`;
443
467
  this.logger.error(message);
444
468
  throw new Error(message);
445
469
  } else return true;
@@ -63,8 +63,8 @@ export class EntityManager {
63
63
  }
64
64
 
65
65
  // Add keys.
66
- _.forEach(keys, (getValue, key) => {
67
- if (overwrite || _.isNil(newItem[key])) newItem[key] = getValue(newItem);
66
+ _.forEach(keys, ({ encode }, key) => {
67
+ if (overwrite || _.isNil(newItem[key])) newItem[key] = encode(newItem);
68
68
  });
69
69
  this.#entityManager.logger.debug('done', newItem);
70
70
 
@@ -90,90 +90,42 @@ export class EntityManager {
90
90
  }
91
91
 
92
92
  /**
93
- * Convert a item into a delimited string containing the properties of a named index.
93
+ * Condense an index object into a delimited string.
94
94
  *
95
95
  * @param {string} entityToken - Entity token.
96
96
  * @param {string} indexToken - Index token.
97
- * @param {object} item - Entity item.
97
+ * @param {object} index - Index object.
98
98
  * @param {string} [delimiter] - Delimiter.
99
99
  * @returns {string} Dehydrated index.
100
100
  * @throws {Error} If entityToken is invalid.
101
101
  * @throws {Error} If indexToken is invalid.
102
102
  * @throws {Error} If item is invalid.
103
103
  */
104
- dehydrateIndex(entityToken, indexToken, item, delimiter = '~') {
104
+ dehydrateIndex(entityToken, indexToken, index, delimiter = '~') {
105
105
  // Validate item.
106
- this.#entityManager.validateItem(item);
107
-
108
- const indexComponents = this.#entityManager.getIndexComponents(
109
- entityToken,
110
- indexToken
111
- );
106
+ this.#entityManager.validateItem(index);
112
107
 
113
- const indexProperties = this.#getIndexProperties(
114
- entityToken,
115
- indexComponents
116
- );
117
-
118
- const { shardKeyToken } = this.#entityManager;
119
- return _.values(
120
- _.pick(
121
- {
122
- ...item,
123
- [shardKeyToken]: this.calcShardKey(entityToken, item),
124
- },
125
- indexProperties
126
- )
127
- ).join(delimiter);
128
- }
129
-
130
- #getIndexProperties = (entityToken, indexComponents) =>
131
- _.sortBy(
132
- _.uniq(
133
- _.flatten(
134
- indexComponents.map((keyToken) =>
135
- getParametersNames(
136
- this.#entityManager.getKeyGenerator(entityToken, keyToken)
137
- )
138
- )
139
- )
140
- )
141
- );
142
-
143
- /**
144
- * Convert a delimited string into a named index key.
145
- *
146
- * @param {string} entityToken - Entity token.
147
- * @param {string} indexToken - Index token.
148
- * @param {string} value - Dehydrated index value.
149
- * @param {string} [delimiter] - Delimiter.
150
- * @returns {object} Rehydrated index key.
151
- * @throws {Error} If entityToken is invalid.
152
- * @throws {Error} If indexToken is invalid.
153
- */
154
- rehydrateIndex(entityToken, indexToken, value = '', delimiter = '~') {
155
108
  const indexComponents = this.#entityManager.getIndexComponents(
156
109
  entityToken,
157
110
  indexToken
158
111
  );
159
112
 
160
- const indexProperties = this.#getIndexProperties(
161
- entityToken,
162
- indexComponents
163
- );
164
-
165
- const item = _.zipObject(indexProperties, value.split(delimiter));
166
-
167
- return indexComponents.reduce(
168
- (index, component) => ({
169
- ...index,
170
- [component]: this.#entityManager.getKeyGenerator(
171
- entityToken,
172
- component
173
- )(item),
174
- }),
175
- {}
176
- );
113
+ const indexProperties = indexComponents.reduce((properties, component) => {
114
+ const { decode } = this.#entityManager.getKey(entityToken, component);
115
+ if (!_.isFunction(decode))
116
+ throw new Error(
117
+ `no decode function defined on ${entityToken} entity key '${component}'`
118
+ );
119
+
120
+ return { ...properties, ...decode(index[component]) };
121
+ }, {});
122
+
123
+ return _.sortedUniqBy(
124
+ _.sortBy(_.entries(indexProperties), ([key]) => key),
125
+ ([key]) => key
126
+ )
127
+ .map(([, value]) => value)
128
+ .join(delimiter);
177
129
  }
178
130
 
179
131
  /**
@@ -202,10 +154,7 @@ export class EntityManager {
202
154
 
203
155
  const result = _.sortedUniq(
204
156
  shardKeySpace.map((shardKey) =>
205
- this.#entityManager.getKeyGenerator(
206
- entityToken,
207
- keyToken
208
- )({
157
+ this.#entityManager.getKey(entityToken, keyToken).encode({
209
158
  ...item,
210
159
  [this.#entityManager.shardKeyToken]: shardKey,
211
160
  })
@@ -312,6 +261,38 @@ export class EntityManager {
312
261
  return result;
313
262
  }
314
263
 
264
+ /**
265
+ * Convert a delimited string into a named index key.
266
+ *
267
+ * @param {string} entityToken - Entity token.
268
+ * @param {string} indexToken - Index token.
269
+ * @param {string} value - Dehydrated index value.
270
+ * @param {string} [delimiter] - Delimiter.
271
+ * @returns {object} Rehydrated index key.
272
+ * @throws {Error} If entityToken is invalid.
273
+ * @throws {Error} If indexToken is invalid.
274
+ */
275
+ rehydrateIndex(entityToken, indexToken, value = '', delimiter = '~') {
276
+ const indexComponents = this.#entityManager.getIndexComponents(
277
+ entityToken,
278
+ indexToken
279
+ );
280
+
281
+ const indexKeys = _.sortBy(
282
+ indexComponents.reduce((keys, component) => {
283
+ const { encode } = this.#entityManager.getKey(entityToken, component);
284
+ return [...new Set([...keys, ...getParametersNames(encode)]).values()];
285
+ }, [])
286
+ );
287
+
288
+ const indexProperies = _.zipObject(indexKeys, value.split(delimiter));
289
+
290
+ return indexComponents.reduce((index, component) => {
291
+ const { encode } = this.#entityManager.getKey(entityToken, component);
292
+ return { ...index, [component]: encode(indexProperies) };
293
+ }, {});
294
+ }
295
+
315
296
  /**
316
297
  * Remove sharded keys from an entity item. Does not mutate original item.
317
298
  *
@@ -67,6 +67,16 @@ const configSchema = {
67
67
  additionalProperties: false,
68
68
  };
69
69
 
70
+ const keySchema = {
71
+ type: 'object',
72
+ properties: {
73
+ encode: {},
74
+ decode: {},
75
+ },
76
+ additionalProperties: false,
77
+ required: ['encode'],
78
+ };
79
+
70
80
  /**
71
81
  * Private EntityManager implementation.
72
82
  *
@@ -151,7 +161,7 @@ export class PrivateEntityManager {
151
161
  shardKeyToken,
152
162
  };
153
163
 
154
- // Validate entity properties.
164
+ // Conform & validate entity properties.
155
165
  _.some(
156
166
  conformedConfig.entities,
157
167
  (
@@ -172,13 +182,25 @@ export class PrivateEntityManager {
172
182
  } else return false;
173
183
  });
174
184
 
175
- // Validate entity keys are functions or undefined.
176
- _.some(keys, (value, key) => {
177
- if (!_.isNil(value) && !_.isFunction(value)) {
178
- const message = `${entityToken} key '${key}' must be a function or nil.`;
185
+ // Conform & validate entity keys.
186
+ _.forEach(keys, (value, key) => {
187
+ keys[key] = _.isFunction(value) ? { encode: value } : value;
188
+
189
+ // Validate key against schema.
190
+ const validatorResult = validate(keys[key], keySchema);
191
+ if (!validatorResult.valid) {
192
+ validatorResult.errors.forEach((error) =>
193
+ this.logger.error(error.message)
194
+ );
195
+ throw new Error(validatorResult.errors);
196
+ }
197
+
198
+ const { decode } = keys[key];
199
+ if (!_.isUndefined(decode) && !_.isFunction(decode)) {
200
+ const message = `entity ${entityKey} key '${key}' decode must be a function or undefined.`;
179
201
  this.logger.error(message);
180
202
  throw new Error(message);
181
- } else return false;
203
+ }
182
204
  });
183
205
 
184
206
  // Validate sharding bump values increase monotonically with keys.
@@ -275,7 +297,7 @@ export class PrivateEntityManager {
275
297
  return indexes[indexToken];
276
298
  }
277
299
 
278
- getKeyGenerator(entityToken, keyToken) {
300
+ getKey(entityToken, keyToken) {
279
301
  const { keys } = this.getEntityConfig(entityToken);
280
302
 
281
303
  if (!_.has(keys, keyToken)) {
@@ -383,6 +405,20 @@ export class PrivateEntityManager {
383
405
  } else return true;
384
406
  }
385
407
 
408
+ /**
409
+ * Tests whether an item is valid.
410
+ *
411
+ * @param {string} item - Entity token.
412
+ * @returns {boolean} true if entityToken is valid.
413
+ */
414
+ validateItem(item) {
415
+ if (!_.isPlainObject(item)) {
416
+ const message = `Invalid item: ${item}`;
417
+ this.logger.error(message);
418
+ throw new Error(message);
419
+ } else return true;
420
+ }
421
+
386
422
  /**
387
423
  * Tests whether a key token is valid.
388
424
  *
@@ -400,20 +436,6 @@ export class PrivateEntityManager {
400
436
  } else return true;
401
437
  }
402
438
 
403
- /**
404
- * Tests whether an item is valid.
405
- *
406
- * @param {string} item - Entity token.
407
- * @returns {boolean} true if entityToken is valid.
408
- */
409
- validateItem(item) {
410
- if (!_.isPlainObject(item)) {
411
- const message = `Invalid item: ${item}`;
412
- this.logger.error(message);
413
- throw new Error(message);
414
- } else return true;
415
- }
416
-
417
439
  /**
418
440
  * Tests whether a timestamp is valid.
419
441
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/entity-manager",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },