@karmaniverous/entity-manager 1.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
@@ -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, item, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
225
+ * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
223
226
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
224
- * [.query(entityToken, keyToken, item, shardQuery, [pageKeys])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
227
+ * [.query(entityToken, keyToken, item, shardQuery, [options])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</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, item, [delimiter])](#module_entity-manager.EntityManager+dehydrateIndex) ⇒ <code>string</code>
246
+ * [.rehydrateIndex(entityToken, indexToken, value, [delimiter])](#module_entity-manager.EntityManager+rehydrateIndex) ⇒ <code>object</code>
241
247
  * [.getKeySpace(entityToken, keyToken, item, timestamp)](#module_entity-manager.EntityManager+getKeySpace) ⇒ <code>Array.&lt;string&gt;</code>
242
- * [.query(entityToken, keyToken, item, shardQuery, [pageKeys])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
248
+ * [.query(entityToken, keyToken, item, shardQuery, [options])](#module_entity-manager.EntityManager+query) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</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,60 @@ 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, item, [delimiter]) ⇒ <code>string</code>
304
+ Convert a item into a delimited string containing the properties of a named index.
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
+ | 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. |
340
+ | [delimiter] | <code>string</code> | <code>&quot;~&quot;</code> | Delimiter. |
341
+
282
342
  <a name="module_entity-manager.EntityManager+getKeySpace"></a>
283
343
 
284
344
  #### entityManager.getKeySpace(entityToken, keyToken, item, timestamp) ⇒ <code>Array.&lt;string&gt;</code>
@@ -303,7 +363,7 @@ Return an array of sharded keys valid for a given entity token & timestamp.
303
363
 
304
364
  <a name="module_entity-manager.EntityManager+query"></a>
305
365
 
306
- #### entityManager.query(entityToken, keyToken, item, shardQuery, [pageKeys]) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
366
+ #### entityManager.query(entityToken, keyToken, item, shardQuery, [options]) ⇒ <code>Promise.&lt;ShardedQueryResult&gt;</code>
307
367
  Query an entity across shards.
308
368
 
309
369
  **Kind**: instance method of [<code>EntityManager</code>](#module_entity-manager.EntityManager)
@@ -315,7 +375,9 @@ Query an entity across shards.
315
375
  | keyToken | <code>string</code> | Key token. |
316
376
  | item | <code>object</code> | Entity item sufficiently populated to generate property keyToken. |
317
377
  | shardQuery | <code>ShardQueryFunction</code> | Sharded query function. |
318
- | [pageKeys] | <code>object</code> | Map of shard page keys. |
378
+ | [options] | <code>object</code> | Options object. |
379
+ | [options.limit] | <code>number</code> | Request limit. |
380
+ | [options.pageKeys] | <code>object</code> | Map of shard page keys. |
319
381
 
320
382
  <a name="module_entity-manager.EntityManager+removeKeys"></a>
321
383
 
@@ -357,7 +419,9 @@ Shard query function
357
419
  | Param | Type | Description |
358
420
  | --- | --- | --- |
359
421
  | shardedKey | <code>string</code> | Sharded key. |
360
- | [pageKey] | <code>\*</code> | Page key. |
422
+ | [options] | <code>object</code> | Options object. |
423
+ | [options.limit] | <code>number</code> | Request limit. |
424
+ | [options.pageKey] | <code>\*</code> | Page key. |
361
425
 
362
426
  <a name="module_entity-manager..ShardedQueryResult"></a>
363
427
 
@@ -4,13 +4,25 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.EntityManager = void 0;
7
- var _isUndefined2 = _interopRequireDefault(require("lodash/isUndefined"));
8
7
  var _map2 = _interopRequireDefault(require("lodash/map"));
8
+ var _size2 = _interopRequireDefault(require("lodash/size"));
9
+ var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty"));
9
10
  var _fromPairs2 = _interopRequireDefault(require("lodash/fromPairs"));
11
+ var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
12
+ var _isInteger2 = _interopRequireDefault(require("lodash/isInteger"));
13
+ var _isUndefined2 = _interopRequireDefault(require("lodash/isUndefined"));
14
+ var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
10
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"));
19
+ var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
20
+ var _pick2 = _interopRequireDefault(require("lodash/pick"));
21
+ var _values2 = _interopRequireDefault(require("lodash/values"));
11
22
  var _forEach2 = _interopRequireDefault(require("lodash/forEach"));
12
23
  var _isNil2 = _interopRequireDefault(require("lodash/isNil"));
13
24
  var _cloneDeep2 = _interopRequireDefault(require("lodash/cloneDeep"));
25
+ var _inspectParametersDeclaration = require("inspect-parameters-declaration");
14
26
  var _PrivateEntityManager = require("./PrivateEntityManager.js");
15
27
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
28
  function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -21,6 +33,7 @@ function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _
21
33
  function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
22
34
  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; } }
23
35
  var _entityManager = /*#__PURE__*/new WeakMap();
36
+ var _getIndexProperties = /*#__PURE__*/new WeakMap();
24
37
  /**
25
38
  * Manage DynamoDb entities.
26
39
  *
@@ -46,6 +59,10 @@ class EntityManager {
46
59
  writable: true,
47
60
  value: void 0
48
61
  });
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
+ });
49
66
  _classPrivateFieldSet(this, _entityManager, new _PrivateEntityManager.PrivateEntityManager({
50
67
  config,
51
68
  logger
@@ -77,15 +94,12 @@ class EntityManager {
77
94
  shardKeyToken
78
95
  } = _classPrivateFieldGet(this, _entityManager);
79
96
  const {
80
- keys,
81
- sharding
97
+ keys
82
98
  } = _classPrivateFieldGet(this, _entityManager).getEntityConfig(entityToken);
83
99
 
84
100
  // Add shardKey.
85
101
  if (overwrite || (0, _isNil2.default)(newItem[shardKeyToken])) {
86
- const entityKey = sharding.entityKey(newItem);
87
- const timestamp = sharding.timestamp(newItem);
88
- newItem[shardKeyToken] = _classPrivateFieldGet(this, _entityManager).getShardKey(entityToken, entityKey, timestamp);
102
+ newItem[shardKeyToken] = this.calcShardKey(entityToken, newItem);
89
103
  }
90
104
 
91
105
  // Add keys.
@@ -99,6 +113,71 @@ class EntityManager {
99
113
  return newItem;
100
114
  }
101
115
 
116
+ /**
117
+ * Calculated the shard key for an entity item.
118
+ *
119
+ * @param {string} entityToken - Entity token.
120
+ * @param {object} item - Entity item.
121
+ * @returns {string} Shard key.
122
+ */
123
+ calcShardKey(entityToken, item) {
124
+ const {
125
+ sharding
126
+ } = _classPrivateFieldGet(this, _entityManager).getEntityConfig(entityToken);
127
+ const entityKey = sharding.entityKey(item);
128
+ const timestamp = sharding.timestamp(item);
129
+ return _classPrivateFieldGet(this, _entityManager).getShardKey(entityToken, entityKey, timestamp);
130
+ }
131
+
132
+ /**
133
+ * Convert a item into a delimited string containing the properties of a named index.
134
+ *
135
+ * @param {string} entityToken - Entity token.
136
+ * @param {string} indexToken - Index token.
137
+ * @param {object} item - Entity item.
138
+ * @param {string} [delimiter] - Delimiter.
139
+ * @returns {string} Dehydrated index.
140
+ * @throws {Error} If entityToken is invalid.
141
+ * @throws {Error} If indexToken is invalid.
142
+ * @throws {Error} If item is invalid.
143
+ */
144
+ dehydrateIndex(entityToken, indexToken, item) {
145
+ let delimiter = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '~';
146
+ // Validate item.
147
+ _classPrivateFieldGet(this, _entityManager).validateItem(item);
148
+ 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
+ }), {});
179
+ }
180
+
102
181
  /**
103
182
  * Return an array of sharded keys valid for a given entity token & timestamp.
104
183
  *
@@ -135,7 +214,9 @@ class EntityManager {
135
214
  *
136
215
  * @callback ShardQueryFunction
137
216
  * @param {string} shardedKey - Sharded key.
138
- * @param {*} [pageKey] - Page key.
217
+ * @param {object} [options] - Options object.
218
+ * @param {number} [options.limit] - Request limit.
219
+ * @param {*} [options.pageKey] - Page key.
139
220
  * @returns {Promise<ShardQueryResult>} Sharded query result.
140
221
  */
141
222
 
@@ -152,31 +233,50 @@ class EntityManager {
152
233
  * @param {string} keyToken - Key token.
153
234
  * @param {object} item - Entity item sufficiently populated to generate property keyToken.
154
235
  * @param {ShardQueryFunction} shardQuery - Sharded query function.
155
- * @param {object} [pageKeys] - Map of shard page keys.
236
+ * @param {object} [options] - Options object.
237
+ * @param {number} [options.limit] - Request limit.
238
+ * @param {object} [options.pageKeys] - Map of shard page keys.
156
239
  * @returns {Promise<ShardedQueryResult>} Sharded query result.
157
240
  */
158
- async query(entityToken, keyToken, item, shardQuery, pageKeys) {
241
+ async query(entityToken, keyToken, item, shardQuery) {
242
+ let {
243
+ limit,
244
+ pageKeys
245
+ } = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
246
+ // Validate params.
247
+ _classPrivateFieldGet(this, _entityManager).validateKeyToken(entityToken, keyToken);
248
+ _classPrivateFieldGet(this, _entityManager).validateItem(item);
249
+ if (!(0, _isFunction2.default)(shardQuery)) throw new Error('shardQuery must be a function');
250
+ if (!(0, _isUndefined2.default)(limit) && !(0, _isInteger2.default)(limit)) throw new Error('limit must be an integer');
251
+ if (!(0, _isUndefined2.default)(pageKeys) && !(0, _isPlainObject2.default)(pageKeys)) throw new Error('pageKeys must be an object');
252
+
159
253
  // Generate default pageKeys if not provided
160
254
  pageKeys ??= (0, _fromPairs2.default)(this.getKeySpace(entityToken, keyToken, item).map(shardedKey => [shardedKey, undefined]));
161
255
 
256
+ // Return empty result if no pageKeys.
257
+ if ((0, _isEmpty2.default)(pageKeys)) return {
258
+ items: [],
259
+ pageKeys: {}
260
+ };
261
+
262
+ // Calculate shard limit.
263
+ const shardLimit = (0, _isUndefined2.default)(limit) ? undefined : Math.ceil(limit / (0, _size2.default)(pageKeys));
264
+
162
265
  // Query every shard in pageKeys.
163
- const shardQueryResults = await Promise.all((0, _map2.default)(pageKeys, (pageKey, shardedKey) => new Promise(resolve => shardQuery(shardedKey, pageKey).then(_ref2 => {
266
+ const shardQueryResults = await Promise.all((0, _map2.default)(pageKeys, (pageKey, shardedKey) => new Promise(resolve => shardQuery(shardedKey, {
267
+ pageKey,
268
+ limit: shardLimit
269
+ }).then(_ref2 => {
164
270
  let {
165
271
  items,
166
272
  pageKey
167
273
  } = _ref2;
168
- console.log('shardQueryResult', {
169
- shardedKey,
170
- items,
171
- pageKey
172
- });
173
274
  return resolve({
174
275
  shardedKey,
175
276
  items,
176
277
  pageKey
177
278
  });
178
279
  }))));
179
- console.log('shardQueryResults', shardQueryResults);
180
280
 
181
281
  // Reduce shardQueryResults into a single result.
182
282
  const result = shardQueryResults.reduce((shardedQueryResult, _ref3) => {
@@ -14,6 +14,8 @@ var _has2 = _interopRequireDefault(require("lodash/has"));
14
14
  var _map2 = _interopRequireDefault(require("lodash/map"));
15
15
  var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));
16
16
  var _isNil2 = _interopRequireDefault(require("lodash/isNil"));
17
+ var _keys2 = _interopRequireDefault(require("lodash/keys"));
18
+ var _difference2 = _interopRequireDefault(require("lodash/difference"));
17
19
  var _some2 = _interopRequireDefault(require("lodash/some"));
18
20
  var _toPairs2 = _interopRequireDefault(require("lodash/toPairs"));
19
21
  var _sortBy2 = _interopRequireDefault(require("lodash/sortBy"));
@@ -43,6 +45,16 @@ const configSchema = {
43
45
  '^\\w+$': {
44
46
  type: 'object',
45
47
  properties: {
48
+ indexes: {
49
+ type: 'object',
50
+ additionalProperties: {
51
+ type: 'array',
52
+ items: {
53
+ type: 'string'
54
+ },
55
+ minItems: 1
56
+ }
57
+ },
46
58
  keys: {
47
59
  type: 'object'
48
60
  },
@@ -160,6 +172,7 @@ class PrivateEntityManager {
160
172
  const conformedConfig = {
161
173
  entities: (0, _mapValues2.default)(entities, _ref => {
162
174
  let {
175
+ indexes = {},
163
176
  keys = {},
164
177
  sharding: {
165
178
  bumps = {},
@@ -170,6 +183,7 @@ class PrivateEntityManager {
170
183
  } = {}
171
184
  } = _ref;
172
185
  return {
186
+ indexes,
173
187
  keys,
174
188
  sharding: {
175
189
  bumps: (0, _fromPairs2.default)((0, _sortBy2.default)((0, _toPairs2.default)(bumps), 0)),
@@ -186,6 +200,7 @@ class PrivateEntityManager {
186
200
  // Validate entity properties.
187
201
  (0, _some2.default)(conformedConfig.entities, (_ref2, entityToken) => {
188
202
  let {
203
+ indexes,
189
204
  keys,
190
205
  sharding: {
191
206
  bumps,
@@ -195,6 +210,16 @@ class PrivateEntityManager {
195
210
  timestamp
196
211
  }
197
212
  } = _ref2;
213
+ // Validate entity index components are entity keys.
214
+ (0, _some2.default)(indexes, (index, indexToken) => {
215
+ const invalidIndexComponents = (0, _difference2.default)(index, (0, _keys2.default)(keys));
216
+ if (invalidIndexComponents.length) {
217
+ const message = `${entityToken} index '${indexToken}' components '${invalidIndexComponents}' are not entity keys.`;
218
+ this.logger.error(message);
219
+ throw new Error(message);
220
+ } else return false;
221
+ });
222
+
198
223
  // Validate entity keys are functions or undefined.
199
224
  (0, _some2.default)(keys, (value, key) => {
200
225
  if (!(0, _isNil2.default)(value) && !(0, _isFunction2.default)(value)) {
@@ -284,6 +309,17 @@ class PrivateEntityManager {
284
309
  this.validateEntityToken(entityToken);
285
310
  return this.config.entities[entityToken];
286
311
  }
312
+ getIndexComponents(entityToken, indexToken) {
313
+ const {
314
+ indexes
315
+ } = this.getEntityConfig(entityToken);
316
+ if (!(0, _has2.default)(indexes, indexToken)) {
317
+ const message = `Index '${indexToken}' does not exist for entity '${entityToken}'.`;
318
+ this.logger.error(message);
319
+ throw new Error(message);
320
+ }
321
+ return indexes[indexToken];
322
+ }
287
323
  getKeyGenerator(entityToken, keyToken) {
288
324
  const {
289
325
  keys
@@ -379,6 +415,22 @@ class PrivateEntityManager {
379
415
  } else return true;
380
416
  }
381
417
 
418
+ /**
419
+ * Tests whether a key token is valid.
420
+ *
421
+ * @param {string} entityToken - Entity token.
422
+ * @param {string} keyToken - Key token.
423
+ * @returns {boolean} true if keyToken is valid.
424
+ */
425
+ validateKeyToken(entityToken, keyToken) {
426
+ this.validateEntityToken(entityToken);
427
+ if (!this.config.entities[entityToken].keys[keyToken]) {
428
+ const message = `Invalid entity ${entityToken} keyToken: ${keyToken}`;
429
+ this.logger.error(message);
430
+ throw new Error(message);
431
+ } else return true;
432
+ }
433
+
382
434
  /**
383
435
  * Tests whether an item is valid.
384
436
  *
@@ -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,17 +55,11 @@ 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.
@@ -79,6 +74,108 @@ 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
+ * Convert a item into a delimited string containing the properties of a named index.
94
+ *
95
+ * @param {string} entityToken - Entity token.
96
+ * @param {string} indexToken - Index token.
97
+ * @param {object} item - Entity item.
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, item, delimiter = '~') {
105
+ // Validate item.
106
+ this.#entityManager.validateItem(item);
107
+
108
+ const indexComponents = this.#entityManager.getIndexComponents(
109
+ entityToken,
110
+ indexToken
111
+ );
112
+
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
+ const indexComponents = this.#entityManager.getIndexComponents(
156
+ entityToken,
157
+ indexToken
158
+ );
159
+
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
+ );
177
+ }
178
+
82
179
  /**
83
180
  * Return an array of sharded keys valid for a given entity token & timestamp.
84
181
  *
@@ -130,7 +227,9 @@ export class EntityManager {
130
227
  *
131
228
  * @callback ShardQueryFunction
132
229
  * @param {string} shardedKey - Sharded key.
133
- * @param {*} [pageKey] - Page key.
230
+ * @param {object} [options] - Options object.
231
+ * @param {number} [options.limit] - Request limit.
232
+ * @param {*} [options.pageKey] - Page key.
134
233
  * @returns {Promise<ShardQueryResult>} Sharded query result.
135
234
  */
136
235
 
@@ -147,10 +246,28 @@ export class EntityManager {
147
246
  * @param {string} keyToken - Key token.
148
247
  * @param {object} item - Entity item sufficiently populated to generate property keyToken.
149
248
  * @param {ShardQueryFunction} shardQuery - Sharded query function.
150
- * @param {object} [pageKeys] - Map of shard page keys.
249
+ * @param {object} [options] - Options object.
250
+ * @param {number} [options.limit] - Request limit.
251
+ * @param {object} [options.pageKeys] - Map of shard page keys.
151
252
  * @returns {Promise<ShardedQueryResult>} Sharded query result.
152
253
  */
153
- async query(entityToken, keyToken, item, shardQuery, pageKeys) {
254
+ async query(
255
+ entityToken,
256
+ keyToken,
257
+ item,
258
+ shardQuery,
259
+ { limit, pageKeys } = {}
260
+ ) {
261
+ // Validate params.
262
+ this.#entityManager.validateKeyToken(entityToken, keyToken);
263
+ this.#entityManager.validateItem(item);
264
+ if (!_.isFunction(shardQuery))
265
+ throw new Error('shardQuery must be a function');
266
+ if (!_.isUndefined(limit) && !_.isInteger(limit))
267
+ throw new Error('limit must be an integer');
268
+ if (!_.isUndefined(pageKeys) && !_.isPlainObject(pageKeys))
269
+ throw new Error('pageKeys must be an object');
270
+
154
271
  // Generate default pageKeys if not provided
155
272
  pageKeys ??= _.fromPairs(
156
273
  this.getKeySpace(entityToken, keyToken, item).map((shardedKey) => [
@@ -159,22 +276,27 @@ export class EntityManager {
159
276
  ])
160
277
  );
161
278
 
279
+ // Return empty result if no pageKeys.
280
+ if (_.isEmpty(pageKeys)) return { items: [], pageKeys: {} };
281
+
282
+ // Calculate shard limit.
283
+ const shardLimit = _.isUndefined(limit)
284
+ ? undefined
285
+ : Math.ceil(limit / _.size(pageKeys));
286
+
162
287
  // Query every shard in pageKeys.
163
288
  const shardQueryResults = await Promise.all(
164
289
  _.map(
165
290
  pageKeys,
166
291
  (pageKey, shardedKey) =>
167
292
  new Promise((resolve) =>
168
- shardQuery(shardedKey, pageKey).then(({ items, pageKey }) => {
169
- console.log('shardQueryResult', { shardedKey, items, pageKey });
170
- return resolve({ shardedKey, items, pageKey });
171
- })
293
+ shardQuery(shardedKey, { pageKey, limit: shardLimit }).then(
294
+ ({ items, pageKey }) => resolve({ shardedKey, items, pageKey })
295
+ )
172
296
  )
173
297
  )
174
298
  );
175
299
 
176
- console.log('shardQueryResults', shardQueryResults);
177
-
178
300
  // Reduce shardQueryResults into a single result.
179
301
  const result = shardQueryResults.reduce(
180
302
  (shardedQueryResult, { shardedKey, items, pageKey }) => ({
@@ -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
  },
@@ -117,6 +127,7 @@ export class PrivateEntityManager {
117
127
  entities: _.mapValues(
118
128
  entities,
119
129
  ({
130
+ indexes = {},
120
131
  keys = {},
121
132
  sharding: {
122
133
  bumps = {},
@@ -126,6 +137,7 @@ export class PrivateEntityManager {
126
137
  timestamp,
127
138
  } = {},
128
139
  }) => ({
140
+ indexes,
129
141
  keys,
130
142
  sharding: {
131
143
  bumps: _.fromPairs(_.sortBy(_.toPairs(bumps), 0)),
@@ -144,11 +156,22 @@ export class PrivateEntityManager {
144
156
  conformedConfig.entities,
145
157
  (
146
158
  {
159
+ indexes,
147
160
  keys,
148
161
  sharding: { bumps, entityKey, nibbleBits, nibbles, timestamp },
149
162
  },
150
163
  entityToken
151
164
  ) => {
165
+ // Validate entity index components are entity keys.
166
+ _.some(indexes, (index, indexToken) => {
167
+ const invalidIndexComponents = _.difference(index, _.keys(keys));
168
+ if (invalidIndexComponents.length) {
169
+ const message = `${entityToken} index '${indexToken}' components '${invalidIndexComponents}' are not entity keys.`;
170
+ this.logger.error(message);
171
+ throw new Error(message);
172
+ } else return false;
173
+ });
174
+
152
175
  // Validate entity keys are functions or undefined.
153
176
  _.some(keys, (value, key) => {
154
177
  if (!_.isNil(value) && !_.isFunction(value)) {
@@ -240,6 +263,18 @@ export class PrivateEntityManager {
240
263
  return this.config.entities[entityToken];
241
264
  }
242
265
 
266
+ getIndexComponents(entityToken, indexToken) {
267
+ const { indexes } = this.getEntityConfig(entityToken);
268
+
269
+ if (!_.has(indexes, indexToken)) {
270
+ const message = `Index '${indexToken}' does not exist for entity '${entityToken}'.`;
271
+ this.logger.error(message);
272
+ throw new Error(message);
273
+ }
274
+
275
+ return indexes[indexToken];
276
+ }
277
+
243
278
  getKeyGenerator(entityToken, keyToken) {
244
279
  const { keys } = this.getEntityConfig(entityToken);
245
280
 
@@ -348,6 +383,23 @@ export class PrivateEntityManager {
348
383
  } else return true;
349
384
  }
350
385
 
386
+ /**
387
+ * Tests whether a key token is valid.
388
+ *
389
+ * @param {string} entityToken - Entity token.
390
+ * @param {string} keyToken - Key token.
391
+ * @returns {boolean} true if keyToken is valid.
392
+ */
393
+ validateKeyToken(entityToken, keyToken) {
394
+ this.validateEntityToken(entityToken);
395
+
396
+ if (!this.config.entities[entityToken].keys[keyToken]) {
397
+ const message = `Invalid entity ${entityToken} keyToken: ${keyToken}`;
398
+ this.logger.error(message);
399
+ throw new Error(message);
400
+ } else return true;
401
+ }
402
+
351
403
  /**
352
404
  * Tests whether an item is valid.
353
405
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/entity-manager",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
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"