@karmaniverous/entity-manager 2.0.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
@@ -220,8 +220,11 @@ This change can be accomplished with no breaking changes to existing implementat
220
220
  * [.EntityManager](#module_entity-manager.EntityManager)
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
+ * [.calcShardKey(entityToken, item)](#module_entity-manager.EntityManager+calcShardKey) ⇒ <code>string</code>
224
+ * [.dehydrateIndex(entityToken, indexToken, index, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
223
225
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
224
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>
225
228
  * [.removeKeys(entityToken, item)](#module_entity-manager.EntityManager+removeKeys) ⇒ <code>object</code>
226
229
  * _inner_
227
230
  * [~ShardQueryResult](#module_entity-manager..ShardQueryResult) : <code>object</code>
@@ -238,8 +241,11 @@ Manage DynamoDb entities.
238
241
  * [.EntityManager](#module_entity-manager.EntityManager)
239
242
  * [new exports.EntityManager(options)](#new_module_entity-manager.EntityManager_new)
240
243
  * [.addKeys(entityToken, item, [overwrite])](#module_entity-manager.EntityManager+addKeys) ⇒ <code>object</code>
244
+ * [.calcShardKey(entityToken, item)](#module_entity-manager.EntityManager+calcShardKey) ⇒ <code>string</code>
245
+ * [.dehydrateIndex(entityToken, indexToken, index, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
241
246
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
242
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>
243
249
  * [.removeKeys(entityToken, item)](#module_entity-manager.EntityManager+removeKeys) ⇒ <code>object</code>
244
250
 
245
251
  <a name="new_module_entity-manager.EntityManager_new"></a>
@@ -279,6 +285,40 @@ Add sharded keys to an entity item. Does not mutate original item.
279
285
  | item | <code>object</code> | | Entity item. |
280
286
  | [overwrite] | <code>boolean</code> | <code>false</code> | Overwrite existing properties. |
281
287
 
288
+ <a name="module_entity-manager.EntityManager+calcShardKey"></a>
289
+
290
+ #### entityManager.calcShardKey(entityToken, item) ⇒ <code>string</code>
291
+ Calculated the shard key for an entity item.
292
+
293
+ **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
294
+ **Returns**: <code>string</code> - Shard key.
295
+
296
+ | Param | Type | Description |
297
+ | --- | --- | --- |
298
+ | entityToken | <code>string</code> | Entity token. |
299
+ | item | <code>object</code> | Entity item. |
300
+
301
+ <a name="module_entity-manager.EntityManager+dehydrateIndex"></a>
302
+
303
+ #### entityManager.dehydrateIndex(entityToken, indexToken, index, [delimiter]) ⇒ <code>string</code>
304
+ Condense an index object into a delimited string.
305
+
306
+ **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
307
+ **Returns**: <code>string</code> - Dehydrated index.
308
+ **Throws**:
309
+
310
+ - <code>Error</code> If entityToken is invalid.
311
+ - <code>Error</code> If indexToken is invalid.
312
+ - <code>Error</code> If item is invalid.
313
+
314
+
315
+ | Param | Type | Default | Description |
316
+ | --- | --- | --- | --- |
317
+ | entityToken | <code>string</code> | | Entity token. |
318
+ | indexToken | <code>string</code> | | Index token. |
319
+ | index | <code>object</code> | | Index object. |
320
+ | [delimiter] | <code>string</code> | <code>&quot;~&quot;</code> | Delimiter. |
321
+
282
322
  <a name="module_entity-manager.EntityManager+getKeySpace"></a>
283
323
 
284
324
  #### entityManager.getKeySpace(entityToken, keyToken, item, timestamp) ⇒ <code>Array.&lt;string&gt;</code>
@@ -319,6 +359,26 @@ Query an entity across shards.
319
359
  | [options.limit] | <code>number</code> | Request limit. |
320
360
  | [options.pageKeys] | <code>object</code> | Map of shard page keys. |
321
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
+
322
382
  <a name="module_entity-manager.EntityManager+removeKeys"></a>
323
383
 
324
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,11 +12,15 @@ 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 _entries2 = _interopRequireDefault(require("lodash/entries"));
17
+ var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
18
+ var _sortedUniqBy2 = _interopRequireDefault(require("lodash/sortedUniqBy"));
19
+ var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
16
20
  var _forEach2 = _interopRequireDefault(require("lodash/forEach"));
17
21
  var _isNil2 = _interopRequireDefault(require("lodash/isNil"));
18
22
  var _cloneDeep2 = _interopRequireDefault(require("lodash/cloneDeep"));
23
+ var _inspectParametersDeclaration = require("inspect-parameters-declaration");
19
24
  var _PrivateEntityManager = require("./PrivateEntityManager.js");
20
25
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
26
  function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -82,20 +87,20 @@ class EntityManager {
82
87
  shardKeyToken
83
88
  } = _classPrivateFieldGet(this, _entityManager);
84
89
  const {
85
- keys,
86
- sharding
90
+ keys
87
91
  } = _classPrivateFieldGet(this, _entityManager).getEntityConfig(entityToken);
88
92
 
89
93
  // Add shardKey.
90
94
  if (overwrite || (0, _isNil2.default)(newItem[shardKeyToken])) {
91
- const entityKey = sharding.entityKey(newItem);
92
- const timestamp = sharding.timestamp(newItem);
93
- newItem[shardKeyToken] = _classPrivateFieldGet(this, _entityManager).getShardKey(entityToken, entityKey, timestamp);
95
+ newItem[shardKeyToken] = this.calcShardKey(entityToken, newItem);
94
96
  }
95
97
 
96
98
  // Add keys.
97
- (0, _forEach2.default)(keys, (getValue, key) => {
98
- 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);
99
104
  });
100
105
  _classPrivateFieldGet(this, _entityManager).logger.debug('done', newItem);
101
106
 
@@ -104,6 +109,61 @@ class EntityManager {
104
109
  return newItem;
105
110
  }
106
111
 
112
+ /**
113
+ * Calculated the shard key for an entity item.
114
+ *
115
+ * @param {string} entityToken - Entity token.
116
+ * @param {object} item - Entity item.
117
+ * @returns {string} Shard key.
118
+ */
119
+ calcShardKey(entityToken, item) {
120
+ const {
121
+ sharding
122
+ } = _classPrivateFieldGet(this, _entityManager).getEntityConfig(entityToken);
123
+ const entityKey = sharding.entityKey(item);
124
+ const timestamp = sharding.timestamp(item);
125
+ return _classPrivateFieldGet(this, _entityManager).getShardKey(entityToken, entityKey, timestamp);
126
+ }
127
+
128
+ /**
129
+ * Condense an index object into a delimited string.
130
+ *
131
+ * @param {string} entityToken - Entity token.
132
+ * @param {string} indexToken - Index token.
133
+ * @param {object} index - Index object.
134
+ * @param {string} [delimiter] - Delimiter.
135
+ * @returns {string} Dehydrated index.
136
+ * @throws {Error} If entityToken is invalid.
137
+ * @throws {Error} If indexToken is invalid.
138
+ * @throws {Error} If item is invalid.
139
+ */
140
+ dehydrateIndex(entityToken, indexToken, index) {
141
+ let delimiter = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '~';
142
+ // Validate item.
143
+ _classPrivateFieldGet(this, _entityManager).validateItem(index);
144
+ const indexComponents = _classPrivateFieldGet(this, _entityManager).getIndexComponents(entityToken, indexToken);
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);
165
+ }
166
+
107
167
  /**
108
168
  * Return an array of sharded keys valid for a given entity token & timestamp.
109
169
  *
@@ -121,7 +181,7 @@ class EntityManager {
121
181
  let timestamp = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : Date.now();
122
182
  _classPrivateFieldGet(this, _entityManager).logger.debug(`getting shard key space for ${entityToken} on key '${keyToken}' at timestamp ${timestamp}...`, item);
123
183
  const shardKeySpace = _classPrivateFieldGet(this, _entityManager).getShardKeySpace(entityToken, timestamp);
124
- 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({
125
185
  ...item,
126
186
  [_classPrivateFieldGet(this, _entityManager).shardKeyToken]: shardKey
127
187
  })));
@@ -192,11 +252,11 @@ class EntityManager {
192
252
  const shardQueryResults = await Promise.all((0, _map2.default)(pageKeys, (pageKey, shardedKey) => new Promise(resolve => shardQuery(shardedKey, {
193
253
  pageKey,
194
254
  limit: shardLimit
195
- }).then(_ref2 => {
255
+ }).then(_ref6 => {
196
256
  let {
197
257
  items,
198
258
  pageKey
199
- } = _ref2;
259
+ } = _ref6;
200
260
  return resolve({
201
261
  shardedKey,
202
262
  items,
@@ -205,12 +265,12 @@ class EntityManager {
205
265
  }))));
206
266
 
207
267
  // Reduce shardQueryResults into a single result.
208
- const result = shardQueryResults.reduce((shardedQueryResult, _ref3) => {
268
+ const result = shardQueryResults.reduce((shardedQueryResult, _ref7) => {
209
269
  let {
210
270
  shardedKey,
211
271
  items,
212
272
  pageKey
213
- } = _ref3;
273
+ } = _ref7;
214
274
  return {
215
275
  items: [...shardedQueryResult.items, ...items],
216
276
  pageKeys: {
@@ -227,6 +287,39 @@ class EntityManager {
227
287
  return result;
228
288
  }
229
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
+
230
323
  /**
231
324
  * Remove sharded keys from an entity item. Does not mutate original item.
232
325
  *
@@ -12,8 +12,12 @@ 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"));
19
+ var _keys2 = _interopRequireDefault(require("lodash/keys"));
20
+ var _difference2 = _interopRequireDefault(require("lodash/difference"));
17
21
  var _some2 = _interopRequireDefault(require("lodash/some"));
18
22
  var _toPairs2 = _interopRequireDefault(require("lodash/toPairs"));
19
23
  var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
@@ -43,6 +47,16 @@ const configSchema = {
43
47
  '^\\w+$': {
44
48
  type: 'object',
45
49
  properties: {
50
+ indexes: {
51
+ type: 'object',
52
+ additionalProperties: {
53
+ type: 'array',
54
+ items: {
55
+ type: 'string'
56
+ },
57
+ minItems: 1
58
+ }
59
+ },
46
60
  keys: {
47
61
  type: 'object'
48
62
  },
@@ -90,6 +104,15 @@ const configSchema = {
90
104
  },
91
105
  additionalProperties: false
92
106
  };
107
+ const keySchema = {
108
+ type: 'object',
109
+ properties: {
110
+ encode: {},
111
+ decode: {}
112
+ },
113
+ additionalProperties: false,
114
+ required: ['encode']
115
+ };
93
116
 
94
117
  /**
95
118
  * Private EntityManager implementation.
@@ -160,6 +183,7 @@ class PrivateEntityManager {
160
183
  const conformedConfig = {
161
184
  entities: (0, _mapValues2.default)(entities, _ref => {
162
185
  let {
186
+ indexes = {},
163
187
  keys = {},
164
188
  sharding: {
165
189
  bumps = {},
@@ -170,6 +194,7 @@ class PrivateEntityManager {
170
194
  } = {}
171
195
  } = _ref;
172
196
  return {
197
+ indexes,
173
198
  keys,
174
199
  sharding: {
175
200
  bumps: (0, _fromPairs2.default)((0, _sortBy2.default)((0, _toPairs2.default)(bumps), 0)),
@@ -183,9 +208,10 @@ class PrivateEntityManager {
183
208
  shardKeyToken
184
209
  };
185
210
 
186
- // Validate entity properties.
211
+ // Conform & validate entity properties.
187
212
  (0, _some2.default)(conformedConfig.entities, (_ref2, entityToken) => {
188
213
  let {
214
+ indexes,
189
215
  keys,
190
216
  sharding: {
191
217
  bumps,
@@ -195,15 +221,38 @@ class PrivateEntityManager {
195
221
  timestamp
196
222
  }
197
223
  } = _ref2;
198
- // Validate entity keys are functions or undefined.
199
- (0, _some2.default)(keys, (value, key) => {
200
- if (!(0, _isNil2.default)(value) && !(0, _isFunction2.default)(value)) {
201
- const message = `${entityToken} key '${key}' must be a function or nil.`;
224
+ // Validate entity index components are entity keys.
225
+ (0, _some2.default)(indexes, (index, indexToken) => {
226
+ const invalidIndexComponents = (0, _difference2.default)(index, (0, _keys2.default)(keys));
227
+ if (invalidIndexComponents.length) {
228
+ const message = `${entityToken} index '${indexToken}' components '${invalidIndexComponents}' are not entity keys.`;
202
229
  this.logger.error(message);
203
230
  throw new Error(message);
204
231
  } else return false;
205
232
  });
206
233
 
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.`;
251
+ this.logger.error(message);
252
+ throw new Error(message);
253
+ }
254
+ });
255
+
207
256
  // Validate sharding bump values increase monotonically with keys.
208
257
  (0, _some2.default)((0, _toPairs2.default)(bumps), (_ref3, i, c) => {
209
258
  let [bump, value] = _ref3;
@@ -284,7 +333,18 @@ class PrivateEntityManager {
284
333
  this.validateEntityToken(entityToken);
285
334
  return this.config.entities[entityToken];
286
335
  }
287
- getKeyGenerator(entityToken, keyToken) {
336
+ getIndexComponents(entityToken, indexToken) {
337
+ const {
338
+ indexes
339
+ } = this.getEntityConfig(entityToken);
340
+ if (!(0, _has2.default)(indexes, indexToken)) {
341
+ const message = `Index '${indexToken}' does not exist for entity '${entityToken}'.`;
342
+ this.logger.error(message);
343
+ throw new Error(message);
344
+ }
345
+ return indexes[indexToken];
346
+ }
347
+ getKey(entityToken, keyToken) {
288
348
  const {
289
349
  keys
290
350
  } = this.getEntityConfig(entityToken);
@@ -380,30 +440,30 @@ class PrivateEntityManager {
380
440
  }
381
441
 
382
442
  /**
383
- * Tests whether a key token is valid.
443
+ * Tests whether an item is valid.
384
444
  *
385
- * @param {string} entityToken - Entity token.
386
- * @param {string} keyToken - Key token.
387
- * @returns {boolean} true if keyToken is valid.
445
+ * @param {string} item - Entity token.
446
+ * @returns {boolean} true if entityToken is valid.
388
447
  */
389
- validateKeyToken(entityToken, keyToken) {
390
- this.validateEntityToken(entityToken);
391
- if (!this.config.entities[entityToken].keys[keyToken]) {
392
- const message = `Invalid entity ${entityToken} keyToken: ${keyToken}`;
448
+ validateItem(item) {
449
+ if (!(0, _isPlainObject2.default)(item)) {
450
+ const message = `Invalid item: ${item}`;
393
451
  this.logger.error(message);
394
452
  throw new Error(message);
395
453
  } else return true;
396
454
  }
397
455
 
398
456
  /**
399
- * Tests whether an item is valid.
457
+ * Tests whether a key token is valid.
400
458
  *
401
- * @param {string} item - Entity token.
402
- * @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.
403
462
  */
404
- validateItem(item) {
405
- if (!(0, _isPlainObject2.default)(item)) {
406
- 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}`;
407
467
  this.logger.error(message);
408
468
  throw new Error(message);
409
469
  } else return true;
@@ -2,6 +2,7 @@
2
2
  * @module entity-manager
3
3
  */
4
4
 
5
+ import { getParametersNames } from 'inspect-parameters-declaration';
5
6
  import _ from 'lodash';
6
7
 
7
8
  import { PrivateEntityManager } from './PrivateEntityManager.js';
@@ -54,22 +55,16 @@ export class EntityManager {
54
55
 
55
56
  // Get entity config.
56
57
  const { shardKeyToken } = this.#entityManager;
57
- const { keys, sharding } = this.#entityManager.getEntityConfig(entityToken);
58
+ const { keys } = this.#entityManager.getEntityConfig(entityToken);
58
59
 
59
60
  // Add shardKey.
60
61
  if (overwrite || _.isNil(newItem[shardKeyToken])) {
61
- const entityKey = sharding.entityKey(newItem);
62
- const timestamp = sharding.timestamp(newItem);
63
- newItem[shardKeyToken] = this.#entityManager.getShardKey(
64
- entityToken,
65
- entityKey,
66
- timestamp
67
- );
62
+ newItem[shardKeyToken] = this.calcShardKey(entityToken, newItem);
68
63
  }
69
64
 
70
65
  // Add keys.
71
- _.forEach(keys, (getValue, key) => {
72
- if (overwrite || _.isNil(newItem[key])) newItem[key] = getValue(newItem);
66
+ _.forEach(keys, ({ encode }, key) => {
67
+ if (overwrite || _.isNil(newItem[key])) newItem[key] = encode(newItem);
73
68
  });
74
69
  this.#entityManager.logger.debug('done', newItem);
75
70
 
@@ -79,6 +74,60 @@ export class EntityManager {
79
74
  return newItem;
80
75
  }
81
76
 
77
+ /**
78
+ * Calculated the shard key for an entity item.
79
+ *
80
+ * @param {string} entityToken - Entity token.
81
+ * @param {object} item - Entity item.
82
+ * @returns {string} Shard key.
83
+ */
84
+ calcShardKey(entityToken, item) {
85
+ const { sharding } = this.#entityManager.getEntityConfig(entityToken);
86
+ const entityKey = sharding.entityKey(item);
87
+ const timestamp = sharding.timestamp(item);
88
+
89
+ return this.#entityManager.getShardKey(entityToken, entityKey, timestamp);
90
+ }
91
+
92
+ /**
93
+ * Condense an index object into a delimited string.
94
+ *
95
+ * @param {string} entityToken - Entity token.
96
+ * @param {string} indexToken - Index token.
97
+ * @param {object} index - Index object.
98
+ * @param {string} [delimiter] - Delimiter.
99
+ * @returns {string} Dehydrated index.
100
+ * @throws {Error} If entityToken is invalid.
101
+ * @throws {Error} If indexToken is invalid.
102
+ * @throws {Error} If item is invalid.
103
+ */
104
+ dehydrateIndex(entityToken, indexToken, index, delimiter = '~') {
105
+ // Validate item.
106
+ this.#entityManager.validateItem(index);
107
+
108
+ const indexComponents = this.#entityManager.getIndexComponents(
109
+ entityToken,
110
+ indexToken
111
+ );
112
+
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);
129
+ }
130
+
82
131
  /**
83
132
  * Return an array of sharded keys valid for a given entity token & timestamp.
84
133
  *
@@ -105,10 +154,7 @@ export class EntityManager {
105
154
 
106
155
  const result = _.sortedUniq(
107
156
  shardKeySpace.map((shardKey) =>
108
- this.#entityManager.getKeyGenerator(
109
- entityToken,
110
- keyToken
111
- )({
157
+ this.#entityManager.getKey(entityToken, keyToken).encode({
112
158
  ...item,
113
159
  [this.#entityManager.shardKeyToken]: shardKey,
114
160
  })
@@ -215,6 +261,38 @@ export class EntityManager {
215
261
  return result;
216
262
  }
217
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
+
218
296
  /**
219
297
  * Remove sharded keys from an entity item. Does not mutate original item.
220
298
  *
@@ -16,6 +16,16 @@ const configSchema = {
16
16
  '^\\w+$': {
17
17
  type: 'object',
18
18
  properties: {
19
+ indexes: {
20
+ type: 'object',
21
+ additionalProperties: {
22
+ type: 'array',
23
+ items: {
24
+ type: 'string',
25
+ },
26
+ minItems: 1,
27
+ },
28
+ },
19
29
  keys: {
20
30
  type: 'object',
21
31
  },
@@ -57,6 +67,16 @@ const configSchema = {
57
67
  additionalProperties: false,
58
68
  };
59
69
 
70
+ const keySchema = {
71
+ type: 'object',
72
+ properties: {
73
+ encode: {},
74
+ decode: {},
75
+ },
76
+ additionalProperties: false,
77
+ required: ['encode'],
78
+ };
79
+
60
80
  /**
61
81
  * Private EntityManager implementation.
62
82
  *
@@ -117,6 +137,7 @@ export class PrivateEntityManager {
117
137
  entities: _.mapValues(
118
138
  entities,
119
139
  ({
140
+ indexes = {},
120
141
  keys = {},
121
142
  sharding: {
122
143
  bumps = {},
@@ -126,6 +147,7 @@ export class PrivateEntityManager {
126
147
  timestamp,
127
148
  } = {},
128
149
  }) => ({
150
+ indexes,
129
151
  keys,
130
152
  sharding: {
131
153
  bumps: _.fromPairs(_.sortBy(_.toPairs(bumps), 0)),
@@ -139,25 +161,48 @@ export class PrivateEntityManager {
139
161
  shardKeyToken,
140
162
  };
141
163
 
142
- // Validate entity properties.
164
+ // Conform & validate entity properties.
143
165
  _.some(
144
166
  conformedConfig.entities,
145
167
  (
146
168
  {
169
+ indexes,
147
170
  keys,
148
171
  sharding: { bumps, entityKey, nibbleBits, nibbles, timestamp },
149
172
  },
150
173
  entityToken
151
174
  ) => {
152
- // Validate entity keys are functions or undefined.
153
- _.some(keys, (value, key) => {
154
- if (!_.isNil(value) && !_.isFunction(value)) {
155
- const message = `${entityToken} key '${key}' must be a function or nil.`;
175
+ // Validate entity index components are entity keys.
176
+ _.some(indexes, (index, indexToken) => {
177
+ const invalidIndexComponents = _.difference(index, _.keys(keys));
178
+ if (invalidIndexComponents.length) {
179
+ const message = `${entityToken} index '${indexToken}' components '${invalidIndexComponents}' are not entity keys.`;
156
180
  this.logger.error(message);
157
181
  throw new Error(message);
158
182
  } else return false;
159
183
  });
160
184
 
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.`;
201
+ this.logger.error(message);
202
+ throw new Error(message);
203
+ }
204
+ });
205
+
161
206
  // Validate sharding bump values increase monotonically with keys.
162
207
  _.some(_.toPairs(bumps), ([bump, value], i, c) => {
163
208
  const [lastBump, lastValue] = i ? c[i - 1] : [];
@@ -240,7 +285,19 @@ export class PrivateEntityManager {
240
285
  return this.config.entities[entityToken];
241
286
  }
242
287
 
243
- getKeyGenerator(entityToken, keyToken) {
288
+ getIndexComponents(entityToken, indexToken) {
289
+ const { indexes } = this.getEntityConfig(entityToken);
290
+
291
+ if (!_.has(indexes, indexToken)) {
292
+ const message = `Index '${indexToken}' does not exist for entity '${entityToken}'.`;
293
+ this.logger.error(message);
294
+ throw new Error(message);
295
+ }
296
+
297
+ return indexes[indexToken];
298
+ }
299
+
300
+ getKey(entityToken, keyToken) {
244
301
  const { keys } = this.getEntityConfig(entityToken);
245
302
 
246
303
  if (!_.has(keys, keyToken)) {
@@ -348,6 +405,20 @@ export class PrivateEntityManager {
348
405
  } else return true;
349
406
  }
350
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
+
351
422
  /**
352
423
  * Tests whether a key token is valid.
353
424
  *
@@ -365,20 +436,6 @@ export class PrivateEntityManager {
365
436
  } else return true;
366
437
  }
367
438
 
368
- /**
369
- * Tests whether an item is valid.
370
- *
371
- * @param {string} item - Entity token.
372
- * @returns {boolean} true if entityToken is valid.
373
- */
374
- validateItem(item) {
375
- if (!_.isPlainObject(item)) {
376
- const message = `Invalid item: ${item}`;
377
- this.logger.error(message);
378
- throw new Error(message);
379
- } else return true;
380
- }
381
-
382
439
  /**
383
440
  * Tests whether a timestamp is valid.
384
441
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/entity-manager",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "license": "BSD-3-Clause",
22
22
  "dependencies": {
23
+ "inspect-parameters-declaration": "^0.1.0",
23
24
  "jsonschema": "^1.4.1",
24
25
  "lodash": "^4.17.21",
25
26
  "string-hash": "^1.1.3"