@karmaniverous/entity-manager 5.0.8 → 5.0.10

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.
Files changed (140) hide show
  1. package/.github/FUNDING.yml +3 -0
  2. package/README.md +1 -2
  3. package/dist/cjs/BaseEntityClient/BaseEntityClient.js +28 -0
  4. package/dist/cjs/BaseQueryBuilder/BaseQueryBuilder.js +62 -0
  5. package/dist/cjs/EntityManager/EntityManager.js +131 -0
  6. package/dist/cjs/EntityManager/ParsedConfig.js +385 -0
  7. package/dist/cjs/EntityManager/addKeys.js +60 -0
  8. package/dist/cjs/EntityManager/createEntityManager.js +46 -0
  9. package/dist/cjs/EntityManager/decodeElement.js +46 -0
  10. package/dist/cjs/EntityManager/decodeGeneratedProperty.js +53 -0
  11. package/dist/cjs/EntityManager/dehydrateIndexItem.js +68 -0
  12. package/dist/cjs/EntityManager/dehydratePageKeyMap.js +93 -0
  13. package/dist/cjs/EntityManager/encodeElement.js +40 -0
  14. package/dist/cjs/EntityManager/encodeGeneratedProperty.js +55 -0
  15. package/dist/cjs/EntityManager/findIndexToken.js +10 -0
  16. package/dist/cjs/EntityManager/getHashKeySpace.js +89 -0
  17. package/dist/cjs/EntityManager/getIndexComponents.js +23 -0
  18. package/dist/cjs/EntityManager/getPrimaryKey.js +76 -0
  19. package/dist/cjs/EntityManager/getShardBump.js +24 -0
  20. package/dist/cjs/EntityManager/query.js +130 -0
  21. package/dist/cjs/EntityManager/rehydrateIndexItem.js +65 -0
  22. package/dist/cjs/EntityManager/rehydratePageKeyMap.js +103 -0
  23. package/dist/cjs/EntityManager/removeKeys.js +51 -0
  24. package/dist/cjs/EntityManager/unwrapIndex.js +59 -0
  25. package/dist/cjs/EntityManager/updateItemHashKey.js +79 -0
  26. package/dist/cjs/EntityManager/updateItemRangeKey.js +65 -0
  27. package/dist/cjs/EntityManager/validateEntityToken.js +17 -0
  28. package/dist/cjs/EntityManager/validateGeneratedProperty.js +23 -0
  29. package/dist/cjs/EntityManager/validateIndexToken.js +16 -0
  30. package/dist/cjs/EntityManager/validateTranscodedProperty.js +16 -0
  31. package/dist/cjs/index.js +15 -0
  32. package/dist/default/lib/EntityManager/EntityManager.js +46 -69
  33. package/dist/default/lib/EntityManager/PrivateEntityManager.js +44 -51
  34. package/dist/index.d.ts +1163 -0
  35. package/dist/mjs/BaseEntityClient/BaseEntityClient.js +26 -0
  36. package/dist/mjs/BaseQueryBuilder/BaseQueryBuilder.js +60 -0
  37. package/dist/mjs/EntityManager/EntityManager.js +129 -0
  38. package/dist/mjs/EntityManager/ParsedConfig.js +383 -0
  39. package/dist/mjs/EntityManager/addKeys.js +58 -0
  40. package/dist/mjs/EntityManager/createEntityManager.js +44 -0
  41. package/dist/mjs/EntityManager/decodeElement.js +44 -0
  42. package/dist/mjs/EntityManager/decodeGeneratedProperty.js +51 -0
  43. package/dist/mjs/EntityManager/dehydrateIndexItem.js +66 -0
  44. package/dist/mjs/EntityManager/dehydratePageKeyMap.js +91 -0
  45. package/dist/mjs/EntityManager/encodeElement.js +38 -0
  46. package/dist/mjs/EntityManager/encodeGeneratedProperty.js +53 -0
  47. package/dist/mjs/EntityManager/findIndexToken.js +8 -0
  48. package/dist/mjs/EntityManager/getHashKeySpace.js +87 -0
  49. package/dist/mjs/EntityManager/getIndexComponents.js +21 -0
  50. package/dist/mjs/EntityManager/getPrimaryKey.js +74 -0
  51. package/dist/mjs/EntityManager/getShardBump.js +22 -0
  52. package/dist/mjs/EntityManager/query.js +128 -0
  53. package/dist/mjs/EntityManager/rehydrateIndexItem.js +63 -0
  54. package/dist/mjs/EntityManager/rehydratePageKeyMap.js +101 -0
  55. package/dist/mjs/EntityManager/removeKeys.js +49 -0
  56. package/dist/mjs/EntityManager/unwrapIndex.js +57 -0
  57. package/dist/mjs/EntityManager/updateItemHashKey.js +77 -0
  58. package/dist/mjs/EntityManager/updateItemRangeKey.js +63 -0
  59. package/dist/mjs/EntityManager/validateEntityToken.js +15 -0
  60. package/dist/mjs/EntityManager/validateGeneratedProperty.js +21 -0
  61. package/dist/mjs/EntityManager/validateIndexToken.js +14 -0
  62. package/dist/mjs/EntityManager/validateTranscodedProperty.js +14 -0
  63. package/dist/mjs/index.js +5 -0
  64. package/docs/.nojekyll +1 -0
  65. package/docs/assets/hierarchy.js +1 -0
  66. package/docs/assets/highlight.css +99 -0
  67. package/docs/assets/icons.js +18 -0
  68. package/docs/assets/icons.svg +1 -0
  69. package/docs/assets/main.js +60 -0
  70. package/docs/assets/navigation.js +1 -0
  71. package/docs/assets/search.js +1 -0
  72. package/docs/assets/style.css +1648 -0
  73. package/docs/classes/index.BaseEntityClient.html +124 -0
  74. package/docs/classes/index.BaseQueryBuilder.html +229 -0
  75. package/docs/classes/index.EntityManager.html +510 -0
  76. package/docs/documents/CHANGELOG.html +1432 -0
  77. package/docs/documents/guides_stan-assistant-guide.html +431 -0
  78. package/docs/functions/index.createEntityManager.html +60 -0
  79. package/docs/hierarchy.html +31 -0
  80. package/docs/index.html +212 -0
  81. package/docs/interfaces/index.BaseConfigMap.html +113 -0
  82. package/docs/interfaces/index.BaseEntityClientOptions.html +82 -0
  83. package/docs/interfaces/index.BaseQueryBuilderOptions.html +92 -0
  84. package/docs/interfaces/index.CapturedConfigMapFrom.html +126 -0
  85. package/docs/interfaces/index.ConfigInput.html +189 -0
  86. package/docs/interfaces/index.ParsedConfig.html +144 -0
  87. package/docs/interfaces/index.ParsedEntityConfig.html +91 -0
  88. package/docs/interfaces/index.ParsedGeneratedPropertiesConfig.html +67 -0
  89. package/docs/interfaces/index.ParsedIndexConfig.html +79 -0
  90. package/docs/interfaces/index.ParsedTranscoder.html +71 -0
  91. package/docs/interfaces/index.QueryOptions.html +180 -0
  92. package/docs/interfaces/index.QueryResult.html +91 -0
  93. package/docs/interfaces/index.ShardBump.html +79 -0
  94. package/docs/interfaces/index.ShardQueryResult.html +93 -0
  95. package/docs/modules/index.html +47 -0
  96. package/docs/modules.html +38 -0
  97. package/docs/sitemap.xml +263 -0
  98. package/docs/types/index.BaseKeyTokens.html +39 -0
  99. package/docs/types/index.Config.html +75 -0
  100. package/docs/types/index.ConfigMap.html +41 -0
  101. package/docs/types/index.ConfigOfClient.html +45 -0
  102. package/docs/types/index.EntitiesFromSchema.html +39 -0
  103. package/docs/types/index.EntityClientItemByToken.html +45 -0
  104. package/docs/types/index.EntityClientRecordByToken.html +43 -0
  105. package/docs/types/index.EntityItem.html +41 -0
  106. package/docs/types/index.EntityItemPartial.html +44 -0
  107. package/docs/types/index.EntityKey.html +40 -0
  108. package/docs/types/index.EntityOfToken.html +39 -0
  109. package/docs/types/index.EntityRecord.html +39 -0
  110. package/docs/types/index.EntityRecordPartial.html +40 -0
  111. package/docs/types/index.EntityToken.html +40 -0
  112. package/docs/types/index.FallbackIndexTokenSet.html +40 -0
  113. package/docs/types/index.HasIndexFor.html +43 -0
  114. package/docs/types/index.HashKeyFrom.html +38 -0
  115. package/docs/types/index.IndexComponentTokens.html +45 -0
  116. package/docs/types/index.IndexHashKeyOf.html +42 -0
  117. package/docs/types/index.IndexRangeKeyOf.html +41 -0
  118. package/docs/types/index.IndexTokensFrom.html +41 -0
  119. package/docs/types/index.IndexTokensOf.html +40 -0
  120. package/docs/types/index.KeysFrom.html +38 -0
  121. package/docs/types/index.PageKey.html +40 -0
  122. package/docs/types/index.PageKeyByIndex.html +46 -0
  123. package/docs/types/index.PresentIndexTokenSet.html +45 -0
  124. package/docs/types/index.Projected.html +39 -0
  125. package/docs/types/index.QueryBuilderQueryOptions.html +45 -0
  126. package/docs/types/index.QueryOptionsByCC.html +49 -0
  127. package/docs/types/index.QueryOptionsByCF.html +47 -0
  128. package/docs/types/index.RangeKeyFrom.html +38 -0
  129. package/docs/types/index.ShardQueryFunction.html +63 -0
  130. package/docs/types/index.ShardQueryMap.html +56 -0
  131. package/docs/types/index.ShardQueryMapByCC.html +49 -0
  132. package/docs/types/index.ShardQueryMapByCF.html +47 -0
  133. package/docs/types/index.ShardedKeysFrom.html +38 -0
  134. package/docs/types/index.StorageItem.html +42 -0
  135. package/docs/types/index.StorageRecord.html +40 -0
  136. package/docs/types/index.TranscodedPropertiesFrom.html +38 -0
  137. package/docs/types/index.UnshardedKeysFrom.html +38 -0
  138. package/docs/types/index.ValidateConfigMap.html +40 -0
  139. package/lib/EntityManager/PrivateEntityManager.js +4 -0
  140. package/package.json +6 -6
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [karmaniverous]
package/README.md CHANGED
@@ -452,5 +452,4 @@ Shard query function
452
452
 
453
453
  ---
454
454
 
455
- See more great templates and other tools on
456
- [my GitHub Profile](https://github.com/karmaniverous)!
455
+ Built with ❤️ on Bali! Find more great tools & templates on [my GitHub Profile](https://github.com/karmaniverous).
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base EntityClient class. Integrates {@link EntityManager | `EntityManager`} with injected logging & enhanced batch processing.
5
+ *
6
+ * @typeParam CC - {@link ConfigMap | `ConfigMap`} that defines an {@link Config | `EntityManager configuration`}'s
7
+ * {@link EntityMap | `EntityMap`}, key properties, and {@link TranscodeRegistry | `TranscodeRegistry`}.
8
+ * If omitted, defaults to {@link BaseConfigMap | `BaseConfigMap`}.
9
+ * @typeParam CF - Values-first config literal type captured by the manager (phantom; type-only). Propagated so
10
+ * client-facing calls that return `IndexTokensOf<CF>` retain the narrowed union.
11
+ *
12
+ * @category EntityClient
13
+ */
14
+ class BaseEntityClient {
15
+ /**
16
+ * Base EntityClient constructor.
17
+ *
18
+ * @param options - {@link BaseEntityClientOptions | `BaseEntityClientOptions`} object.
19
+ */
20
+ constructor(options) {
21
+ const { batchProcessOptions = {}, entityManager, logger = console, } = options;
22
+ this.batchProcessOptions = batchProcessOptions;
23
+ this.entityManager = entityManager;
24
+ this.logger = logger;
25
+ }
26
+ }
27
+
28
+ exports.BaseEntityClient = BaseEntityClient;
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var radash = require('radash');
4
+
5
+ /* eslint-disable @typescript-eslint/no-unused-vars */
6
+ /**
7
+ * Abstract base class supporting a fluent API for building a {@link ShardQueryMap | `ShardQueryMap`} using a database client.
8
+ *
9
+ * @typeParam CC - {@link ConfigMap | `ConfigMap`} that defines an {@link Config | `EntityManager configuration`}'s {@link EntityMap | `EntityMap`}, key properties, and {@link TranscodeRegistry | `TranscodeRegistry`}. If omitted, defaults to {@link BaseConfigMap | `BaseConfigMap`}.
10
+ * @typeParam EntityClient - {@link BaseEntityClient | `BaseEntityClient`} derived class instance.
11
+ * @typeParam IndexParams - Database platform-specific, index-specific query parameters.
12
+ * @typeParam CF - Optional values-first config literal type for page key narrowing.
13
+ * @typeParam K - Optional projection keys; narrows item shape when provided.
14
+ *
15
+ * @category QueryBuilder
16
+ */
17
+ class BaseQueryBuilder {
18
+ /** BaseQueryBuilder constructor. */
19
+ constructor(options) {
20
+ /**
21
+ * Maps `indexToken` values to database platform-specific query parameters.
22
+ *
23
+ * @protected
24
+ */
25
+ this.indexParamsMap = {};
26
+ const { entityClient, entityToken, hashKeyToken, pageKeyMap } = options;
27
+ this.entityClient = entityClient;
28
+ this.entityToken = entityToken;
29
+ this.hashKeyToken = hashKeyToken;
30
+ this.pageKeyMap = pageKeyMap;
31
+ }
32
+ /**
33
+ * Builds a {@link ShardQueryMap | `ShardQueryMap`} object.
34
+ *
35
+ * @returns - The {@link ShardQueryMap | `ShardQueryMap`} object.
36
+ */
37
+ build() {
38
+ return radash.mapValues(this.indexParamsMap, (_indexConfig, indexToken) => this.getShardQueryFunction(indexToken));
39
+ }
40
+ /**
41
+ * Execute the built query across shards and indexes via {@link EntityManager.query | `EntityManager.query`}.
42
+ *
43
+ * @param options - Query options excluding `entityToken`, `pageKeyMap`, and `shardQueryMap`, which are supplied by the builder.
44
+ *
45
+ * @returns The merged, de-duplicated, sorted query result, including a compact `pageKeyMap` token for the next page.
46
+ *
47
+ * @remarks
48
+ * This delegates orchestration to Entity Manager; provider-specific behavior lives in {@link getShardQueryFunction | `getShardQueryFunction`}.
49
+ */
50
+ async query(options) {
51
+ const { entityClient: { entityManager }, entityToken, pageKeyMap, } = this;
52
+ const shardQueryMap = this.build();
53
+ return await entityManager.query({
54
+ ...options,
55
+ entityToken,
56
+ pageKeyMap,
57
+ shardQueryMap,
58
+ });
59
+ }
60
+ }
61
+
62
+ exports.BaseQueryBuilder = BaseQueryBuilder;
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ var tslib = require('tslib');
4
+ var addKeys = require('./addKeys.js');
5
+ var encodeGeneratedProperty = require('./encodeGeneratedProperty.js');
6
+ var findIndexToken = require('./findIndexToken.js');
7
+ var getPrimaryKey = require('./getPrimaryKey.js');
8
+ var ParsedConfig = require('./ParsedConfig.js');
9
+ var query = require('./query.js');
10
+ var removeKeys = require('./removeKeys.js');
11
+
12
+ var _EntityManager_config;
13
+ /**
14
+ * The EntityManager class applies a configuration-driven sharded data model &
15
+ * query strategy to NoSql data.
16
+ *
17
+ * @typeParam CC - {@link ConfigMap | `ConfigMap`} that defines the configuration's {@link EntityMap | `EntityMap`}, key properties, and {@link TranscodeRegistry | `TranscodeRegistry`}. If omitted, defaults to {@link BaseConfigMap | `BaseConfigMap`}.
18
+ * @typeParam CF - Values-first config literal type captured at construction
19
+ * time (phantom generic; type-only). This is used by downstream
20
+ * adapters to infer index-token unions (ITS) and per-index page
21
+ * key shapes.
22
+ *
23
+ * @remarks
24
+ * While the {@link EntityManager.query | `query`} method is `public`, normally it should not be called directly. The `query` method is used by a platform-specific {@link BaseQueryBuilder.query | `QueryBuilder.query`} method to provide a fluent query API.
25
+ *
26
+ * @category EntityManager
27
+ */
28
+ class EntityManager {
29
+ /**
30
+ * Create an EntityManager instance.
31
+ *
32
+ * @param config - EntityManager {@link Config | `Config`} object.
33
+ * @param logger - Logger object (defaults to `console`, must support `debug` & `error` methods).
34
+ */
35
+ constructor(config, logger = console) {
36
+ _EntityManager_config.set(this, void 0);
37
+ // Accept a compile-time-only `entitiesSchema` key on values-first configs.
38
+ // We strip it here so the same literal config can be used with either the
39
+ // factory or the direct constructor without tripping Zod's strict parser.
40
+ //
41
+ // This keeps runtime semantics unchanged and avoids widening ParsedConfig.
42
+ const cfgWithOptionalES = config;
43
+ const { entitiesSchema: _ignored, ...configForParse } = cfgWithOptionalES;
44
+ tslib.__classPrivateFieldSet(this, _EntityManager_config, ParsedConfig.configSchema.parse(configForParse), "f");
45
+ this.logger = logger;
46
+ }
47
+ /**
48
+ * Get the current EntityManager {@link Config | `Config`} object.
49
+ *
50
+ * @returns Current {@link Config | `Config`} object.
51
+ */
52
+ get config() {
53
+ return tslib.__classPrivateFieldGet(this, _EntityManager_config, "f");
54
+ }
55
+ /**
56
+ * Set the current EntityManager {@link Config | `Config`} object.
57
+ *
58
+ * @param value - {@link Config | `Config`} object.
59
+ */
60
+ set config(value) {
61
+ tslib.__classPrivateFieldSet(this, _EntityManager_config, ParsedConfig.configSchema.parse(value), "f");
62
+ }
63
+ /**
64
+ * Encode a generated property value. Returns a string or undefined if atomicity requirement of sharded properties not met.
65
+ *
66
+ * @param property - {@link Config | Config} `generatedProperties` key.
67
+ * @param item - {@link StorageItem | `StorageItem`} object.
68
+ *
69
+ * @returns Encoded generated property value.
70
+ *
71
+ * @throws `Error` if `property` is not a {@link Config | Config} `generatedProperties` key.
72
+ */
73
+ encodeGeneratedProperty(property, item) {
74
+ return encodeGeneratedProperty.encodeGeneratedProperty(this, property, item);
75
+ }
76
+ addKeys(entityToken, i, overwrite = false) {
77
+ if (Array.isArray(i)) {
78
+ return i.map((item) => addKeys.addKeys(this, entityToken, item, overwrite));
79
+ }
80
+ return addKeys.addKeys(this, entityToken, i, overwrite);
81
+ }
82
+ getPrimaryKey(entityToken, i, overwrite = false) {
83
+ if (Array.isArray(i)) {
84
+ return i.flatMap((item) => getPrimaryKey.getPrimaryKey(this, entityToken, item, overwrite));
85
+ }
86
+ return getPrimaryKey.getPrimaryKey(this, entityToken, i, overwrite);
87
+ }
88
+ removeKeys(entityToken, i) {
89
+ if (Array.isArray(i)) {
90
+ const out = i.map((item) => removeKeys.removeKeys(this, entityToken, item));
91
+ return out;
92
+ }
93
+ const out = removeKeys.removeKeys(this, entityToken, i);
94
+ return out;
95
+ }
96
+ findIndexToken(hashKeyToken, rangeKeyToken, suppressError) {
97
+ // Dispatch with a literal to satisfy overload selection.
98
+ return suppressError === true
99
+ ? findIndexToken.findIndexToken(this, hashKeyToken, rangeKeyToken, true)
100
+ : findIndexToken.findIndexToken(this, hashKeyToken, rangeKeyToken, false);
101
+ }
102
+ /**
103
+ * Query a database entity across shards in a provider-generic fashion.
104
+ *
105
+ * @remarks
106
+ * The provided `shardQueryMap` performs the actual query of individual data pages on individual index/shard combinations.
107
+ *
108
+ * Individual shard query results will be combined, deduped by {@link Config | `Config`} `uniqueProperty` value, and sorted by {@link QueryOptions.sortOrder | `sortOrder`}.
109
+ *
110
+ * In queries on sharded data, expect the leading and trailing edges of returned data pages to interleave somewhat with preceding & following pages.
111
+ *
112
+ * Unsharded query results should sort & page as expected.
113
+ *
114
+ * **Normally this method should not be called directly!** It is used by a platform-specific {@link BaseQueryBuilder.query | `QueryBuilder.query`} method to provide a fluent query API.
115
+ *
116
+ * @param options - {@link QueryOptions | `QueryOptions`} object.
117
+ *
118
+ * @returns {@link QueryResult} object.
119
+ *
120
+ * @throws Error if `options` {@link QueryOptions.pageKeyMap | `pageKeyMap`} `pageKeyMap` keys do not match {@link QueryOptions.shardQueryMap | `shardQueryMap`} keys.
121
+ *
122
+ * @protected
123
+ */
124
+ async query(options) {
125
+ const result = await query.query(this, options);
126
+ return result;
127
+ }
128
+ }
129
+ _EntityManager_config = new WeakMap();
130
+
131
+ exports.EntityManager = EntityManager;
@@ -0,0 +1,385 @@
1
+ 'use strict';
2
+
3
+ var entityTools = require('@karmaniverous/entity-tools');
4
+ var radash = require('radash');
5
+ var zod = require('zod');
6
+
7
+ const defaultShardBump = { timestamp: 0, charBits: 1, chars: 0 };
8
+ const validateArrayUnique = (arr, ctx, identity = (item) => item, path = []) => {
9
+ const counts = radash.counting(arr, identity);
10
+ for (const [element, count] of Object.entries(counts)) {
11
+ if (count > 1)
12
+ ctx.addIssue({
13
+ code: 'custom',
14
+ message: `duplicate array element`,
15
+ params: { element },
16
+ path,
17
+ });
18
+ }
19
+ };
20
+ const validateKeysExclusive = (keys, label, ref, ctx) => {
21
+ const intersection = keys.filter((key) => ref.includes(key));
22
+ if (intersection.length)
23
+ ctx.addIssue({
24
+ code: 'custom',
25
+ message: `${label} key collision: ${intersection.toString()}`,
26
+ });
27
+ };
28
+ const componentArray = zod.z
29
+ .array(zod.z.string().min(1))
30
+ .nonempty()
31
+ .superRefine(validateArrayUnique);
32
+ /**
33
+ * Runtime configuration schema for {@link EntityManager | `EntityManager`}.
34
+ *
35
+ * @remarks
36
+ * This is authoritative at runtime and used internally by the {@link EntityManager | `EntityManager`} constructor.
37
+ * It is also used by tests. It is not intended as a user-facing TypeDoc artifact.
38
+ *
39
+ * @hidden
40
+ */
41
+ const configSchema = zod.z
42
+ .object({
43
+ entities: zod.z
44
+ .record(zod.z.string(), zod.z
45
+ .object({
46
+ defaultLimit: zod.z
47
+ .number()
48
+ .int()
49
+ .positive()
50
+ .optional()
51
+ .default(10)
52
+ .describe('Default max items returned by EntityManager.query for this entity (across all shards).'),
53
+ defaultPageSize: zod.z
54
+ .number()
55
+ .int()
56
+ .positive()
57
+ .optional()
58
+ .default(10)
59
+ .describe('Default per-shard page size used by EntityManager.query for this entity.'),
60
+ shardBumps: zod.z
61
+ .array(zod.z
62
+ .object({
63
+ timestamp: zod.z
64
+ .number()
65
+ .int()
66
+ .nonnegative()
67
+ .describe('Start timestamp (ms) for this shard bump (inclusive).'),
68
+ charBits: zod.z
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .max(5)
73
+ .describe('Bits per shard character (radix = 2**charBits).'),
74
+ chars: zod.z
75
+ .number()
76
+ .int()
77
+ .min(0)
78
+ .max(40)
79
+ .describe('Shard suffix width (chars); controls shard space.'),
80
+ })
81
+ .strict())
82
+ .optional()
83
+ .default([defaultShardBump])
84
+ // validate shardBump uniqueness by timestamp.
85
+ .superRefine((val, ctx) => {
86
+ validateArrayUnique(val, ctx, ({ timestamp }) => timestamp, [
87
+ 'timestamp',
88
+ ]);
89
+ })
90
+ .transform((val) => {
91
+ // sort shardBumps by timestamp.
92
+ let sorted = radash.sort(val, ({ timestamp }) => timestamp);
93
+ // prepend defaultShardBump if missing zero-timestamp bump.
94
+ if (sorted[0].timestamp !== 0) {
95
+ sorted = [defaultShardBump, ...sorted];
96
+ }
97
+ return sorted;
98
+ })
99
+ // validate shardBump chars mootonically increase with timestamp.
100
+ .superRefine((val, ctx) => {
101
+ if (val.length > 1) {
102
+ for (let i = 1; i < val.length; i++)
103
+ if (val[i].chars <= val[i - 1].chars)
104
+ ctx.addIssue({
105
+ code: 'custom',
106
+ message: `shardBump chars do not monotonically increase at timestamp ${val[i].timestamp.toString()}`,
107
+ path: [i],
108
+ });
109
+ }
110
+ }),
111
+ timestampProperty: zod.z
112
+ .string()
113
+ .min(1)
114
+ .describe('Property token whose value selects the shard bump (typically a timestamp).'),
115
+ uniqueProperty: zod.z
116
+ .string()
117
+ .min(1)
118
+ .describe('Property token used to dedupe and build the global range key.'),
119
+ })
120
+ .strict())
121
+ .optional()
122
+ .default({})
123
+ .describe('Entity definitions keyed by entity token.'),
124
+ generatedProperties: zod.z
125
+ .object({
126
+ sharded: zod.z
127
+ .record(zod.z.string(), componentArray)
128
+ .optional()
129
+ .default({})
130
+ .describe('Sharded generated property tokens (hash-side); atomic encoding semantics.'),
131
+ unsharded: zod.z
132
+ .record(zod.z.string(), componentArray)
133
+ .optional()
134
+ .default({})
135
+ .describe('Unsharded generated property tokens (range-side); non-atomic encoding semantics.'),
136
+ })
137
+ .optional()
138
+ .default({ sharded: {}, unsharded: {} }),
139
+ hashKey: zod.z.string().describe('Global hash key property name.'),
140
+ indexes: zod.z
141
+ .record(zod.z.string(), zod.z.object({
142
+ hashKey: zod.z
143
+ .string()
144
+ .min(1)
145
+ .describe('Index hash key token (global hash key or a sharded generated key).'),
146
+ rangeKey: zod.z
147
+ .string()
148
+ .min(1)
149
+ .describe('Index range key token (global range key, an unsharded generated key, or a transcoded property).'),
150
+ projections: zod.z
151
+ .array(zod.z.string().min(1))
152
+ .superRefine(validateArrayUnique)
153
+ .optional(),
154
+ }))
155
+ .optional()
156
+ .default({})
157
+ .describe('Index definitions keyed by index token.'),
158
+ generatedKeyDelimiter: zod.z
159
+ .string()
160
+ .regex(/\W+/)
161
+ .optional()
162
+ .default('|')
163
+ .describe('Delimiter between generated key elements (default `|`).'),
164
+ generatedValueDelimiter: zod.z
165
+ .string()
166
+ .regex(/\W+/)
167
+ .optional()
168
+ .default('#')
169
+ .describe('Delimiter between generated element name and value (default `#`).'),
170
+ propertyTranscodes: zod.z.record(zod.z.string(), zod.z.string()).optional().default({}),
171
+ rangeKey: zod.z.string().describe('Global range key property name.'),
172
+ shardKeyDelimiter: zod.z
173
+ .string()
174
+ .regex(/\W+/)
175
+ .optional()
176
+ .default('!')
177
+ .describe('Delimiter between entity token and shard suffix in hash key values.'),
178
+ throttle: zod.z
179
+ .number()
180
+ .int()
181
+ .positive()
182
+ .optional()
183
+ .default(10)
184
+ .describe('Default max concurrency for shard queries during EntityManager.query.'),
185
+ transcodes: zod.z
186
+ .record(zod.z.string(), zod.z
187
+ .object({
188
+ // Accept function shapes without relying on z.function()
189
+ // to avoid TS inference conflicts across Zod versions and
190
+ // to remain compatible with narrower parameter types.
191
+ encode: zod.z
192
+ .custom((fn) => typeof fn === 'function')
193
+ .describe('Encode a value to a lexicographically sortable string.'),
194
+ decode: zod.z
195
+ .custom((fn) => typeof fn === 'function')
196
+ .describe('Decode a previously encoded string back to the value type.'),
197
+ })
198
+ .strict())
199
+ .optional()
200
+ .default(entityTools.defaultTranscodes),
201
+ })
202
+ .strict()
203
+ .superRefine((data, ctx) => {
204
+ // validate no generated key delimiter collision
205
+ if (data.generatedKeyDelimiter.includes(data.generatedValueDelimiter))
206
+ ctx.addIssue({
207
+ code: 'custom',
208
+ message: 'generatedKeyDelimiter contains generatedValueDelimiter',
209
+ params: {
210
+ generatedKeyDelimiter: data.generatedKeyDelimiter,
211
+ generatedValueDelimiter: data.generatedValueDelimiter,
212
+ },
213
+ path: ['generatedKeyDelimiter'],
214
+ });
215
+ if (data.generatedKeyDelimiter.includes(data.shardKeyDelimiter))
216
+ ctx.addIssue({
217
+ code: 'custom',
218
+ message: 'generatedKeyDelimiter contains shardKeyDelimiter',
219
+ params: {
220
+ generatedKeyDelimiter: data.generatedKeyDelimiter,
221
+ shardKeyDelimiter: data.shardKeyDelimiter,
222
+ },
223
+ path: ['generatedKeyDelimiter'],
224
+ });
225
+ // validate no generated value delimiter collision
226
+ if (data.generatedValueDelimiter.includes(data.generatedKeyDelimiter))
227
+ ctx.addIssue({
228
+ code: 'custom',
229
+ message: 'generatedValueDelimiter contains generatedKeyDelimiter',
230
+ params: {
231
+ generatedValueDelimiter: data.generatedValueDelimiter,
232
+ generatedKeyDelimiter: data.generatedKeyDelimiter,
233
+ },
234
+ path: ['generatedValueDelimiter'],
235
+ });
236
+ if (data.generatedValueDelimiter.includes(data.shardKeyDelimiter))
237
+ ctx.addIssue({
238
+ code: 'custom',
239
+ message: 'generatedValueDelimiter contains shardKeyDelimiter',
240
+ params: {
241
+ generatedValueDelimiter: data.generatedValueDelimiter,
242
+ shardKeyDelimiter: data.shardKeyDelimiter,
243
+ },
244
+ path: ['generatedValueDelimiter'],
245
+ });
246
+ // validate no shard key delimiter collision
247
+ if (data.shardKeyDelimiter.includes(data.generatedKeyDelimiter))
248
+ ctx.addIssue({
249
+ code: 'custom',
250
+ message: 'shardKeyDelimiter contains generatedKeyDelimiter',
251
+ params: {
252
+ generatedKeyDelimiter: data.generatedKeyDelimiter,
253
+ shardKeyDelimiter: data.shardKeyDelimiter,
254
+ },
255
+ path: ['shardKeyDelimiter'],
256
+ });
257
+ if (data.shardKeyDelimiter.includes(data.generatedValueDelimiter))
258
+ ctx.addIssue({
259
+ code: 'custom',
260
+ message: 'shardKeyDelimiter contains generatedValueDelimiter',
261
+ params: {
262
+ generatedValueDelimiter: data.generatedValueDelimiter,
263
+ shardKeyDelimiter: data.shardKeyDelimiter,
264
+ },
265
+ path: ['shardKeyDelimiter'],
266
+ });
267
+ // get reserved keys
268
+ const shardedKeys = Object.keys(data.generatedProperties.sharded);
269
+ const unshardedKeys = Object.keys(data.generatedProperties.unsharded);
270
+ const transcodedProperties = Object.keys(data.propertyTranscodes);
271
+ // validate hashKey exclusive.
272
+ validateKeysExclusive([data.hashKey], 'hashKey', [
273
+ data.rangeKey,
274
+ ...shardedKeys,
275
+ ...unshardedKeys,
276
+ ...transcodedProperties,
277
+ ], ctx);
278
+ // validate rangeKey exclusive.
279
+ validateKeysExclusive([data.rangeKey], 'rangeKey', [...shardedKeys, ...unshardedKeys, ...transcodedProperties], ctx);
280
+ // validate shardedKeys exclusive.
281
+ validateKeysExclusive(shardedKeys, 'shardedKeys', [...unshardedKeys, ...transcodedProperties], ctx);
282
+ // validate unshardedKeys exclusive.
283
+ validateKeysExclusive(unshardedKeys, 'unshardedKeys', transcodedProperties, ctx);
284
+ // validate all propertyTranscode values are transcode keys.
285
+ const transcodes = Object.keys(data.transcodes);
286
+ for (const [property, transcode] of Object.entries(data.propertyTranscodes))
287
+ if (!transcodes.includes(transcode))
288
+ ctx.addIssue({
289
+ code: 'custom',
290
+ message: `propertyTranscodes['${property}'] references unknown transcode '${transcode}'`,
291
+ path: ['propertyTranscodes', property],
292
+ });
293
+ // Validate all sharded property elements are transcoded properties.
294
+ for (const [property, elements] of Object.entries(data.generatedProperties.sharded))
295
+ for (const element of elements)
296
+ if (!transcodedProperties.includes(element))
297
+ ctx.addIssue({
298
+ code: 'custom',
299
+ message: `generatedProperties.sharded['${property}'] contains non-transcoded element '${element}'`,
300
+ path: ['generatedProperties', 'sharded', property],
301
+ });
302
+ // Validate all unsharded property elements are transcoded properties.
303
+ for (const [property, elements] of Object.entries(data.generatedProperties.unsharded))
304
+ for (const element of elements)
305
+ if (!transcodedProperties.includes(element))
306
+ ctx.addIssue({
307
+ code: 'custom',
308
+ message: `generatedProperties.unsharded['${property}'] contains non-transcoded element '${element}'`,
309
+ path: ['generatedProperties', 'unsharded', property],
310
+ });
311
+ // Validate indexes.
312
+ // Validate no two indexes have the same hashKey & rangeKey.
313
+ const seenIndexPairs = new Set();
314
+ for (const [indexKey, { hashKey, rangeKey, projections }] of Object.entries(data.indexes)) {
315
+ const pairSig = `${hashKey}|${rangeKey}`;
316
+ if (seenIndexPairs.has(pairSig)) {
317
+ ctx.addIssue({
318
+ code: 'custom',
319
+ message: `duplicate index hashKey/rangeKey pair`,
320
+ path: ['indexes', indexKey],
321
+ });
322
+ }
323
+ else
324
+ seenIndexPairs.add(pairSig);
325
+ // Validate hash key is sharded.
326
+ if (![data.hashKey, ...shardedKeys].includes(hashKey)) {
327
+ ctx.addIssue({
328
+ code: 'custom',
329
+ message: `index '${indexKey}' hashKey '${hashKey}' must be one of [${[
330
+ data.hashKey,
331
+ ...shardedKeys,
332
+ ].join(', ')}]`,
333
+ path: ['indexes', indexKey, 'hashKey'],
334
+ });
335
+ }
336
+ // Validate range key is unsharded or transcodable.
337
+ if (![data.rangeKey, ...unshardedKeys, ...transcodedProperties].includes(rangeKey)) {
338
+ ctx.addIssue({
339
+ code: 'custom',
340
+ message: `index '${indexKey}' rangeKey '${rangeKey}' must be one of [${[
341
+ data.rangeKey,
342
+ ...unshardedKeys,
343
+ ...transcodedProperties,
344
+ ].join(', ')}]`,
345
+ path: ['indexes', indexKey, 'rangeKey'],
346
+ });
347
+ }
348
+ // Validate no index projections are keys.
349
+ if (projections)
350
+ for (const projection of projections)
351
+ if ([
352
+ data.hashKey,
353
+ data.rangeKey,
354
+ hashKey,
355
+ rangeKey,
356
+ ...shardedKeys,
357
+ ...unshardedKeys,
358
+ ].includes(projection))
359
+ ctx.addIssue({
360
+ code: 'custom',
361
+ message: 'index projection is a key',
362
+ params: { projection },
363
+ path: ['indexes', indexKey, 'projections'],
364
+ });
365
+ }
366
+ // validate entities
367
+ for (const [entityToken, { timestampProperty, uniqueProperty },] of Object.entries(data.entities)) {
368
+ // validate timestampProperty is a transcoded property.
369
+ if (!transcodedProperties.includes(timestampProperty))
370
+ ctx.addIssue({
371
+ code: 'custom',
372
+ message: `entities['${entityToken}'].timestampProperty '${timestampProperty}' must be one of [${transcodedProperties.join(', ')}]`,
373
+ path: ['entities', entityToken, 'timestampProperty'],
374
+ });
375
+ // validate uniqueProperty is a transcoded property.
376
+ if (!transcodedProperties.includes(uniqueProperty))
377
+ ctx.addIssue({
378
+ code: 'custom',
379
+ message: `entities['${entityToken}'].uniqueProperty '${uniqueProperty}' must be one of [${transcodedProperties.join(', ')}]`,
380
+ path: ['entities', entityToken, 'uniqueProperty'],
381
+ });
382
+ }
383
+ });
384
+
385
+ exports.configSchema = configSchema;
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ var entityTools = require('@karmaniverous/entity-tools');
4
+ var encodeGeneratedProperty = require('./encodeGeneratedProperty.js');
5
+ var updateItemHashKey = require('./updateItemHashKey.js');
6
+ var updateItemRangeKey = require('./updateItemRangeKey.js');
7
+ var validateEntityToken = require('./validateEntityToken.js');
8
+
9
+ /**
10
+ * Update generated properties, hash key, and range key on an {@link EntityItem | `EntityItem`} object.
11
+ *
12
+ * @param entityManager - {@link EntityManager | `EntityManager`} instance.
13
+ * @param entityToken - {@link ConfigEntity.uniqueProperty | `this.config.entities`} key.
14
+ * @param item - {@link EntityItem | `EntityItem`} object.
15
+ * @param overwrite - Overwrite existing properties (default `false`).
16
+ *
17
+ * @returns {@link EntityRecord | `EntityRecord`} object with updated properties.
18
+ *
19
+ * @throws `Error` if `entityToken` is invalid.
20
+ */
21
+ function addKeys(entityManager, entityToken, item, overwrite = false) {
22
+ try {
23
+ // Validate params.
24
+ validateEntityToken.validateEntityToken(entityManager, entityToken);
25
+ // Update hash key.
26
+ let newItem = updateItemHashKey.updateItemHashKey(entityManager, entityToken, item, overwrite);
27
+ // Update range key.
28
+ newItem = updateItemRangeKey.updateItemRangeKey(entityManager, entityToken, newItem, overwrite);
29
+ // Update generated properties.
30
+ const { sharded, unsharded } = entityManager.config.generatedProperties;
31
+ for (const property in { ...sharded, ...unsharded }) {
32
+ if (overwrite || entityTools.isNil(item[property])) {
33
+ const encoded = encodeGeneratedProperty.encodeGeneratedProperty(entityManager, property, newItem);
34
+ if (encoded)
35
+ Object.assign(newItem, { [property]: encoded });
36
+ else
37
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
38
+ delete newItem[property];
39
+ }
40
+ }
41
+ entityManager.logger.debug('updated entity item generated properties', {
42
+ item,
43
+ entityToken,
44
+ overwrite,
45
+ newItem,
46
+ });
47
+ return newItem;
48
+ }
49
+ catch (error) {
50
+ if (error instanceof Error)
51
+ entityManager.logger.error(error.message, {
52
+ item,
53
+ entityToken,
54
+ overwrite,
55
+ });
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ exports.addKeys = addKeys;