@karmaniverous/entity-manager 7.0.1 → 7.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 +389 -259
- package/dist/cjs/BaseQueryBuilder/BaseQueryBuilder.js +1 -0
- package/dist/cjs/EntityManager/EntityManager.js +8 -1
- package/dist/cjs/EntityManager/query.js +9 -3
- package/dist/index.d.ts +40 -22
- package/dist/mjs/BaseQueryBuilder/BaseQueryBuilder.js +1 -0
- package/dist/mjs/EntityManager/EntityManager.js +8 -1
- package/dist/mjs/EntityManager/query.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,261 +2,392 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@karmaniverous/entity-manager)  <!-- TYPEDOC_EXCLUDE --> [](https://docs.karmanivero.us/entity-manager) [](https://github.com/karmaniverous/entity-manager/tree/main/CHANGELOG.md)<!-- /TYPEDOC_EXCLUDE --> [](https://github.com/karmaniverous/entity-manager/tree/main/LICENSE.md)
|
|
4
4
|
|
|
5
|
-
Entity Manager implements rational indexing & cross‑shard querying at scale in your NoSQL database so you can focus on application logic. It is provider‑agnostic (great fit for DynamoDB) and TypeScript‑first with strong types and runtime validation.
|
|
6
|
-
|
|
7
|
-
Key links:
|
|
8
|
-
|
|
9
|
-
- Docs: https://docs.karmanivero.us/entity-manager
|
|
10
|
-
- Discussions: https://github.com/karmaniverous/entity-manager/discussions
|
|
11
|
-
|
|
12
|
-
## Why this library?
|
|
13
|
-
|
|
14
|
-
Modern NoSQL puts the burden of indexing, sharding, and pagination on the application. Entity Manager gives you:
|
|
15
|
-
|
|
16
|
-
- Values‑first configuration with runtime validation (Zod) and best‑in‑class inference.
|
|
17
|
-
- Token‑aware and index‑aware types end‑to‑end (entities, keys, page keys, queries).
|
|
18
|
-
- Deterministic sharding with a time‑based scale‑up schedule.
|
|
19
|
-
- Cross‑shard, multi‑index query orchestration with dedupe and sorting.
|
|
20
|
-
- Dehydration/rehydration of page keys to pass a single compact token between calls.
|
|
21
|
-
|
|
22
|
-
## Install
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npm install @karmaniverous/entity-manager
|
|
26
|
-
# optional (tests/demo helpers)
|
|
27
|
-
npm install --save-dev @karmaniverous/mock-db
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## DX highlights
|
|
31
|
-
|
|
32
|
-
- Values‑first + schema‑first config:
|
|
33
|
-
- Use a config literal (prefer `as const` and `satisfies`) to preserve literal tokens.
|
|
34
|
-
- Optionally provide Zod schemas (`entitiesSchema`) to infer entity shapes without generics.
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
- CC
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} as const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
5
|
+
Entity Manager implements rational indexing & cross‑shard querying at scale in your NoSQL database so you can focus on application logic. It is provider‑agnostic (great fit for DynamoDB) and TypeScript‑first with strong types and runtime validation.
|
|
6
|
+
|
|
7
|
+
Key links:
|
|
8
|
+
|
|
9
|
+
- Docs: https://docs.karmanivero.us/entity-manager
|
|
10
|
+
- Discussions: https://github.com/karmaniverous/entity-manager/discussions
|
|
11
|
+
|
|
12
|
+
## Why this library?
|
|
13
|
+
|
|
14
|
+
Modern NoSQL puts the burden of indexing, sharding, and pagination on the application. Entity Manager gives you:
|
|
15
|
+
|
|
16
|
+
- Values‑first configuration with runtime validation (Zod) and best‑in‑class inference.
|
|
17
|
+
- Token‑aware and index‑aware types end‑to‑end (entities, keys, page keys, queries).
|
|
18
|
+
- Deterministic sharding with a time‑based scale‑up schedule.
|
|
19
|
+
- Cross‑shard, multi‑index query orchestration with dedupe and sorting.
|
|
20
|
+
- Dehydration/rehydration of page keys to pass a single compact token between calls.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @karmaniverous/entity-manager
|
|
26
|
+
# optional (tests/demo helpers)
|
|
27
|
+
npm install --save-dev @karmaniverous/mock-db
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## DX highlights
|
|
31
|
+
|
|
32
|
+
- Values‑first + schema‑first config:
|
|
33
|
+
- Use a config literal (prefer `as const` and `satisfies`) to preserve literal tokens.
|
|
34
|
+
- Optionally provide Zod schemas (`entitiesSchema`) to infer entity shapes without generics.
|
|
35
|
+
- Projection‑aware typing (type‑only K):
|
|
36
|
+
- Pass attributes as a const tuple (K) through your query types to narrow result items to Pick<…> of those properties.
|
|
37
|
+
- No runtime change; adapters execute projections. Adapters should auto‑include `uniqueProperty` and any explicit sort keys when callers omit them to preserve dedupe/sort invariants.
|
|
38
|
+
- Index‑aware typing (values‑first config literal “CF” and captured config “CC” helpers):
|
|
39
|
+
- CF (values‑first config literal): drive index token unions and page‑key narrowing directly from a values‑first config literal (`QueryOptionsByCF`, `ShardQueryMapByCF`).
|
|
40
|
+
- CC (Captured Config): derive index tokens from a captured config type while reusing CF for narrowing (`QueryOptionsByCC`, `ShardQueryMapByCC`).
|
|
41
|
+
- Token‑aware helpers:
|
|
42
|
+
- `addKeys`, `getPrimaryKey`, `removeKeys` narrow types by entity token (no casts).
|
|
43
|
+
- Index‑aware page keys (optional CF channel):
|
|
44
|
+
- Provide a values‑first config literal (CF) with `indexes` and get typed page keys per index.
|
|
45
|
+
- Use `QueryOptionsByCF` and `ShardQueryMapByCF` to derive index token unions directly from CF.
|
|
46
|
+
- CC-based DX sugar (values-first captured config):
|
|
47
|
+
- Use `QueryOptionsByCC` and `ShardQueryMapByCC` to derive index token unions from a captured config type (via `IndexTokensFrom`), while still benefiting from page-key narrowing.
|
|
48
|
+
|
|
49
|
+
## Quick start (values‑first + schema‑first)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { z } from 'zod';
|
|
53
|
+
import {
|
|
54
|
+
createEntityManager,
|
|
55
|
+
defaultTranscodes,
|
|
56
|
+
} from '@karmaniverous/entity-manager';
|
|
57
|
+
|
|
58
|
+
// 1) Schema-first entity shapes (non-generated fields only)
|
|
59
|
+
const userSchema = z.object({
|
|
60
|
+
userId: z.string(), // unique property
|
|
61
|
+
created: z.number(), // timestamp property
|
|
62
|
+
updated: z.number().optional(),
|
|
63
|
+
firstNameCanonical: z.string(),
|
|
64
|
+
lastNameCanonical: z.string(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 2) Values-first config literal (prefer `as const`)
|
|
68
|
+
const config = {
|
|
69
|
+
hashKey: 'hashKey2',
|
|
70
|
+
rangeKey: 'rangeKey',
|
|
71
|
+
generatedProperties: {
|
|
72
|
+
sharded: {
|
|
73
|
+
userPK: ['userId'] as const,
|
|
74
|
+
},
|
|
75
|
+
unsharded: {
|
|
76
|
+
firstNameRK: ['firstNameCanonical', 'lastNameCanonical'] as const,
|
|
77
|
+
lastNameRK: ['lastNameCanonical', 'firstNameCanonical'] as const,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
propertyTranscodes: {
|
|
81
|
+
userId: 'string',
|
|
82
|
+
created: 'timestamp',
|
|
83
|
+
updated: 'timestamp',
|
|
84
|
+
firstNameCanonical: 'string',
|
|
85
|
+
lastNameCanonical: 'string',
|
|
86
|
+
},
|
|
87
|
+
indexes: {
|
|
88
|
+
created: { hashKey: 'hashKey2', rangeKey: 'created' },
|
|
89
|
+
userCreated: { hashKey: 'userPK', rangeKey: 'created' },
|
|
90
|
+
firstName: { hashKey: 'hashKey2', rangeKey: 'firstNameRK' },
|
|
91
|
+
lastName: { hashKey: 'hashKey2', rangeKey: 'lastNameRK' },
|
|
92
|
+
} as const,
|
|
93
|
+
entities: {
|
|
94
|
+
user: {
|
|
95
|
+
uniqueProperty: 'userId',
|
|
96
|
+
timestampProperty: 'created',
|
|
97
|
+
shardBumps: [{ timestamp: Date.now(), charBits: 2, chars: 1 }],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
entitiesSchema: { user: userSchema },
|
|
101
|
+
transcodes: defaultTranscodes,
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
// 3) Create the manager — types captured from values, shapes from schemas
|
|
105
|
+
const manager = createEntityManager(config);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Token‑aware helpers
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Input item (no generated keys yet)
|
|
112
|
+
const user = {
|
|
113
|
+
userId: 'u1',
|
|
114
|
+
created: Date.now(),
|
|
115
|
+
firstNameCanonical: 'lee',
|
|
116
|
+
lastNameCanonical: 'zhang',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add generated keys (hashKey/rangeKey + index tokens)
|
|
120
|
+
const record = manager.addKeys('user', user);
|
|
121
|
+
|
|
122
|
+
// Compute one or more primary keys
|
|
123
|
+
const keys = manager.getPrimaryKey('user', { userId: 'u1' });
|
|
124
|
+
|
|
125
|
+
// Strip generated keys after read
|
|
126
|
+
const item = manager.removeKeys('user', record);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Types narrow automatically from the entity token (`'user'`). No casts required.
|
|
130
|
+
|
|
131
|
+
## Index‑aware querying (values‑first config literal, “CF” channel)
|
|
132
|
+
|
|
133
|
+
When you author a values‑first config literal with `indexes` (prefer `as const`), Entity Manager can:
|
|
134
|
+
|
|
135
|
+
- Constrain `shardQueryMap` keys to the index key union.
|
|
136
|
+
- Narrow page‑key shapes per index (only its component tokens).
|
|
137
|
+
- Derive ITS (index token subset) automatically from CF via `QueryOptionsByCF` and `ShardQueryMapByCF`.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import type {
|
|
141
|
+
ShardQueryFunction,
|
|
142
|
+
ShardQueryMapByCF,
|
|
143
|
+
QueryOptionsByCF,
|
|
144
|
+
} from '@karmaniverous/entity-manager';
|
|
145
|
+
|
|
146
|
+
// CF: capture index tokens from a values-first literal
|
|
147
|
+
const cf = {
|
|
148
|
+
indexes: {
|
|
149
|
+
firstName: { hashKey: 'hashKey2', rangeKey: 'firstNameRK' },
|
|
150
|
+
lastName: { hashKey: 'hashKey2', rangeKey: 'lastNameRK' },
|
|
151
|
+
},
|
|
152
|
+
} as const;
|
|
153
|
+
type CF = typeof cf;
|
|
154
|
+
|
|
155
|
+
// SQFs are typed; pageKey is narrowed to index components per IT
|
|
156
|
+
const firstNameSQF: ShardQueryFunction<
|
|
157
|
+
MyConfigMap,
|
|
158
|
+
'user',
|
|
159
|
+
'firstName',
|
|
160
|
+
CF
|
|
161
|
+
> = async (hashKey, pageKey, pageSize) => ({ count: 0, items: [], pageKey });
|
|
162
|
+
const lastNameSQF: ShardQueryFunction<
|
|
163
|
+
MyConfigMap,
|
|
164
|
+
'user',
|
|
165
|
+
'lastName',
|
|
166
|
+
CF
|
|
167
|
+
> = async (hashKey, pageKey, pageSize) => ({ count: 0, items: [], pageKey });
|
|
168
|
+
|
|
169
|
+
// CF-aware shardQueryMap — only 'firstName' | 'lastName' allowed
|
|
170
|
+
const shardQueryMap: ShardQueryMapByCF<MyConfigMap, 'user', CF> = {
|
|
171
|
+
firstName: firstNameSQF,
|
|
172
|
+
lastName: lastNameSQF,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Derive ITS from CF for options
|
|
176
|
+
const options: QueryOptionsByCF<MyConfigMap, 'user', CF> = {
|
|
177
|
+
entityToken: 'user',
|
|
178
|
+
item: {},
|
|
179
|
+
shardQueryMap,
|
|
180
|
+
limit: 50,
|
|
181
|
+
pageSize: 10,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = await manager.query(options);
|
|
185
|
+
// result.pageKeyMap is a compact string — pass it to the next call’s options.pageKeyMap
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Captured Config (“CC”) aliases
|
|
189
|
+
|
|
190
|
+
You can also derive ITS (index token subset) directly from a values‑first captured config type (CC) using `QueryOptionsByCC` and `ShardQueryMapByCC`. This mirrors the CF helpers but drives ITS from the CC type (via `IndexTokensFrom`) and passes the same CC through the CF channel for page‑key narrowing.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import type {
|
|
194
|
+
ShardQueryFunction,
|
|
195
|
+
ShardQueryMapByCC,
|
|
196
|
+
QueryOptionsByCC,
|
|
197
|
+
} from '@karmaniverous/entity-manager';
|
|
198
|
+
|
|
199
|
+
// A values-first config literal capturing index tokens (the same shape used for CF)
|
|
200
|
+
const cc = {
|
|
201
|
+
indexes: {
|
|
202
|
+
firstName: { hashKey: 'hashKey2', rangeKey: 'firstNameRK' },
|
|
203
|
+
lastName: { hashKey: 'hashKey2', rangeKey: 'lastNameRK' },
|
|
204
|
+
},
|
|
205
|
+
} as const;
|
|
206
|
+
type CC = typeof cc;
|
|
207
|
+
|
|
208
|
+
// Reuse typed SQFs (pageKey narrowed per index)
|
|
209
|
+
const firstNameSQF: ShardQueryFunction<
|
|
210
|
+
MyConfigMap,
|
|
211
|
+
'user',
|
|
212
|
+
'firstName',
|
|
213
|
+
CC
|
|
214
|
+
> = async (hashKey, pageKey, pageSize) => ({ count: 0, items: [], pageKey });
|
|
215
|
+
const lastNameSQF: ShardQueryFunction<
|
|
216
|
+
MyConfigMap,
|
|
217
|
+
'user',
|
|
218
|
+
'lastName',
|
|
219
|
+
CC
|
|
220
|
+
> = async (hashKey, pageKey, pageSize) => ({ count: 0, items: [], pageKey });
|
|
221
|
+
|
|
222
|
+
// CC-aware shardQueryMap — only 'firstName' | 'lastName' allowed
|
|
223
|
+
const shardQueryMapCC: ShardQueryMapByCC<MyConfigMap, 'user', CC> = {
|
|
224
|
+
firstName: firstNameSQF,
|
|
225
|
+
lastName: lastNameSQF,
|
|
226
|
+
};
|
|
227
|
+
const optionsCC: QueryOptionsByCC<MyConfigMap, 'user', CC> = {
|
|
228
|
+
entityToken: 'user',
|
|
229
|
+
item: {},
|
|
230
|
+
shardQueryMap: shardQueryMapCC,
|
|
231
|
+
};
|
|
232
|
+
const resultCC = await manager.query(optionsCC);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Notes:
|
|
236
|
+
|
|
237
|
+
- Entity Manager enumerates hash‑key space for the time window, rehydrates page keys (when present), executes shard queries in parallel (throttled), dedupes by unique property, sorts, and dehydrates a new pageKeyMap.
|
|
238
|
+
- For provider integration, the SQF lambda encapsulates the platform‑specific query for one index + shard page. See tests and entity‑client‑dynamodb for examples.
|
|
239
|
+
|
|
240
|
+
## Projection‑aware typing (K)
|
|
241
|
+
|
|
242
|
+
Entity Manager supports a type‑only projection channel K that narrows result item shapes when a provider adapter projects a subset of attributes at runtime. Pass your attributes as a const tuple and thread K through `ShardQueryFunction/Map`, `QueryOptions`, and `QueryResult`.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import type {
|
|
246
|
+
ConfigMap,
|
|
247
|
+
EntityItemByToken,
|
|
248
|
+
QueryOptions,
|
|
249
|
+
QueryResult,
|
|
250
|
+
ShardQueryFunction,
|
|
251
|
+
ShardQueryMap,
|
|
252
|
+
} from '@karmaniverous/entity-manager';
|
|
253
|
+
|
|
254
|
+
// Minimal entities (example)
|
|
255
|
+
interface Email {
|
|
256
|
+
created: number;
|
|
257
|
+
email: string;
|
|
258
|
+
userId: string;
|
|
259
|
+
}
|
|
260
|
+
interface User {
|
|
261
|
+
beneficiaryId: string;
|
|
262
|
+
created: number;
|
|
263
|
+
firstNameCanonical: string;
|
|
264
|
+
lastNameCanonical: string;
|
|
265
|
+
phone?: string;
|
|
266
|
+
updated: number;
|
|
267
|
+
userId: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type MyConfigMap = ConfigMap<{
|
|
271
|
+
EntityMap: { email: Email; user: User };
|
|
272
|
+
HashKey: 'hashKey2';
|
|
273
|
+
RangeKey: 'rangeKey';
|
|
274
|
+
ShardedKeys: 'beneficiaryPK' | 'userPK';
|
|
275
|
+
UnshardedKeys: 'firstNameRK' | 'lastNameRK' | 'phoneRK';
|
|
276
|
+
TranscodedProperties:
|
|
277
|
+
| 'beneficiaryId'
|
|
278
|
+
| 'created'
|
|
279
|
+
| 'email'
|
|
280
|
+
| 'firstNameCanonical'
|
|
281
|
+
| 'lastNameCanonical'
|
|
282
|
+
| 'phone'
|
|
283
|
+
| 'updated'
|
|
284
|
+
| 'userId';
|
|
285
|
+
}>;
|
|
286
|
+
|
|
287
|
+
// CF capturing a single index
|
|
288
|
+
const cf = {
|
|
289
|
+
indexes: {
|
|
290
|
+
firstName: { hashKey: 'hashKey2', rangeKey: 'firstNameRK' },
|
|
291
|
+
},
|
|
292
|
+
} as const;
|
|
293
|
+
type CF = typeof cf;
|
|
294
|
+
|
|
295
|
+
// Projection attributes as const tuple — narrows K.
|
|
296
|
+
const attrs = ['userId', 'created'] as const;
|
|
297
|
+
type K = typeof attrs;
|
|
298
|
+
|
|
299
|
+
// A typed SQF: pageKey narrows via CF; items narrow via K (type-only).
|
|
300
|
+
const sqf: ShardQueryFunction<MyConfigMap, 'user', 'firstName', CF, K> = async (
|
|
301
|
+
_hashKey,
|
|
302
|
+
_pageKey,
|
|
303
|
+
_pageSize,
|
|
304
|
+
) => ({
|
|
305
|
+
count: 0,
|
|
306
|
+
items: [], // never[] is assignable to the projected array
|
|
307
|
+
pageKey: _pageKey,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ShardQueryMap carrying CF and K.
|
|
311
|
+
const map: ShardQueryMap<MyConfigMap, 'user', 'firstName', CF, K> = {
|
|
312
|
+
firstName: sqf,
|
|
313
|
+
};
|
|
314
|
+
const options: QueryOptions<MyConfigMap, 'user', 'firstName', CF, K> = {
|
|
315
|
+
entityToken: 'user',
|
|
316
|
+
item: {},
|
|
317
|
+
shardQueryMap: map,
|
|
318
|
+
};
|
|
319
|
+
const result: QueryResult<MyConfigMap, 'user', 'firstName', K> =
|
|
320
|
+
await manager.query(options);
|
|
321
|
+
// result.items: Pick<EntityItemByToken<MyConfigMap, 'user'>, 'userId' | 'created'>[]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Notes:
|
|
325
|
+
|
|
326
|
+
- K is a type‑only channel; it does not change runtime behavior. Providers (e.g., DynamoDB adapters) execute projections.
|
|
327
|
+
- Dedupe/sort invariants: Entity Manager dedupes by `uniqueProperty` and applies `QueryOptions.sortOrder`. If your adapter projects attributes, ensure it auto‑includes `uniqueProperty` and any explicit sort keys when callers omit them from K.
|
|
328
|
+
|
|
329
|
+
## Page keys in a nutshell
|
|
330
|
+
|
|
331
|
+
- `rehydratePageKeyMap` decodes a dehydrated array (compressed string) into a two‑layer map of `{ indexToken: { hashKeyValue: pageKey | undefined } }`.
|
|
332
|
+
- `dehydratePageKeyMap` performs the inverse and emits a compact array (compressed in `query()`).
|
|
333
|
+
- You rarely call these directly — `query()` composes them for you — but the API is exposed for advanced flows.
|
|
334
|
+
|
|
335
|
+
## ESM / CJS
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// ESM
|
|
339
|
+
import {
|
|
340
|
+
createEntityManager,
|
|
341
|
+
defaultTranscodes,
|
|
342
|
+
} from '@karmaniverous/entity-manager';
|
|
343
|
+
|
|
344
|
+
// CJS
|
|
345
|
+
const {
|
|
346
|
+
createEntityManager,
|
|
347
|
+
defaultTranscodes,
|
|
348
|
+
} = require('@karmaniverous/entity-manager');
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Logging
|
|
352
|
+
|
|
353
|
+
Entity Manager logs debug and error details via the injected logger (defaults to `console`).
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const logger = { debug: () => undefined, error: console.error };
|
|
357
|
+
const manager = createEntityManager(config, logger);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Types you’ll reach for
|
|
361
|
+
|
|
362
|
+
- Values/schema capture
|
|
363
|
+
- `createEntityManager(config, logger?)`
|
|
364
|
+
- `ConfigInput` (values‑first), `CapturedConfigMapFrom`, `EntitiesFromSchema`
|
|
365
|
+
- Token aware
|
|
366
|
+
- `EntityToken<CC>`, `EntityItemByToken<CC, ET>`, `EntityRecordByToken<CC, ET>`
|
|
367
|
+
- Index aware (values‑first config literal, “CF” channel)
|
|
368
|
+
- `PageKeyByIndex<CC, ET, IT, CF>`
|
|
369
|
+
- `ShardQueryFunction<CC, ET, IT, CF>`, `ShardQueryMap<CC, ET, ITS, CF>`
|
|
370
|
+
- `QueryOptions<CC, ET, ITS, CF>`, `QueryResult<CC, ET, ITS>`
|
|
371
|
+
- DX sugar: `IndexTokensOf<CF>`, `QueryOptionsByCF`, `ShardQueryMapByCF`, `IndexTokensFrom<CC>`, `QueryOptionsByCC`, `ShardQueryMapByCC`
|
|
372
|
+
- Projection helpers
|
|
373
|
+
- `KeysFrom<K>`
|
|
374
|
+
- `Projected<T, K>`
|
|
375
|
+
- `ProjectedItemByToken<CC, ET, K>`
|
|
376
|
+
|
|
377
|
+
See the full API: https://docs.karmanivero.us/entity-manager
|
|
378
|
+
|
|
379
|
+
## Scripts (repo)
|
|
380
|
+
|
|
381
|
+
- build: rollup outputs ESM/CJS + .d.ts
|
|
382
|
+
- test: vitest with coverage
|
|
383
|
+
- lint: ESLint (type‑aware) + Prettier
|
|
384
|
+
- docs: TypeDoc
|
|
385
|
+
- typecheck: tsc + tsd (type‑level tests)
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
BSD‑3‑Clause (see package.json).
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
Built for you with ❤️ on Bali! Find more great tools & templates on [my GitHub Profile](https://github.com/karmaniverous).
|
|
@@ -9,6 +9,7 @@ var radash = require('radash');
|
|
|
9
9
|
* @typeParam EntityClient - {@link BaseEntityClient | `BaseEntityClient`} derived class instance.
|
|
10
10
|
* @typeParam IndexParams - Database platform-specific, index-specific query parameters.
|
|
11
11
|
* @typeParam CF - Optional values-first config literal type for page key narrowing.
|
|
12
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
12
13
|
*
|
|
13
14
|
* @category QueryBuilder
|
|
14
15
|
*/
|
|
@@ -30,7 +30,14 @@ class EntityManager {
|
|
|
30
30
|
*/
|
|
31
31
|
constructor(config, logger = console) {
|
|
32
32
|
_EntityManager_config.set(this, void 0);
|
|
33
|
-
|
|
33
|
+
// Accept a compile-time-only `entitiesSchema` key on values-first configs.
|
|
34
|
+
// We strip it here so the same literal config can be used with either the
|
|
35
|
+
// factory or the direct constructor without tripping Zod's strict parser.
|
|
36
|
+
//
|
|
37
|
+
// This keeps runtime semantics unchanged and avoids widening ParsedConfig.
|
|
38
|
+
const cfgWithOptionalES = config;
|
|
39
|
+
const { entitiesSchema: _ignored, ...configForParse } = cfgWithOptionalES;
|
|
40
|
+
tslib.__classPrivateFieldSet(this, _EntityManager_config, ParsedConfig.configSchema.parse(configForParse), "f");
|
|
34
41
|
this.logger = logger;
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
@@ -28,8 +28,10 @@ const { compressToEncodedURIComponent, decompressFromEncodedURIComponent } = lzS
|
|
|
28
28
|
*/
|
|
29
29
|
async function query(entityManager, options) {
|
|
30
30
|
try {
|
|
31
|
-
// Get defaults.
|
|
32
|
-
const
|
|
31
|
+
// Get defaults (avoid unsafe destructuring on generic access).
|
|
32
|
+
const entityDefaults = entityManager.config.entities[options.entityToken];
|
|
33
|
+
const defaultLimit = entityDefaults.defaultLimit;
|
|
34
|
+
const defaultPageSize = entityDefaults.defaultPageSize;
|
|
33
35
|
// Extract params.
|
|
34
36
|
const { entityToken, limit = defaultLimit, item, pageKeyMap, pageSize = defaultPageSize, shardQueryMap, sortOrder = [], timestampFrom = 0, timestampTo = Date.now(), throttle = entityManager.config.throttle, } = options;
|
|
35
37
|
// Validate params.
|
|
@@ -98,8 +100,12 @@ async function query(entityManager, options) {
|
|
|
98
100
|
break;
|
|
99
101
|
} while (workingResult.items.length < limit);
|
|
100
102
|
// Dedupe & sort working result.
|
|
101
|
-
|
|
103
|
+
// Note: when projecting, callers may omit uniqueProperty/sort keys.
|
|
104
|
+
// We perform operations on the full item shape and then cast back.
|
|
105
|
+
const itemsForOps = workingResult.items;
|
|
106
|
+
const dedupedSorted = entityTools.sort(radash.unique(itemsForOps, (i) => i[entityManager.config.entities[entityToken]
|
|
102
107
|
.uniqueProperty].toString()), sortOrder);
|
|
108
|
+
workingResult.items = dedupedSorted;
|
|
103
109
|
const result = {
|
|
104
110
|
count: workingResult.items.length,
|
|
105
111
|
items: workingResult.items,
|
package/dist/index.d.ts
CHANGED
|
@@ -256,6 +256,18 @@ type EntityOfToken<CC extends BaseConfigMap, ET extends EntityToken<CC>> = Exact
|
|
|
256
256
|
type EntityItemByToken<CC extends BaseConfigMap, ET extends EntityToken<CC>> = Partial<EntityOfToken<CC, ET> & Record<CC['HashKey'] | CC['RangeKey'] | CC['ShardedKeys'] | CC['UnshardedKeys'], string>> & Record<string, unknown>;
|
|
257
257
|
/** EntityRecordByToken — database-facing record (keys required) narrowed to a specific entity token. */
|
|
258
258
|
type EntityRecordByToken<CC extends BaseConfigMap, ET extends EntityToken<CC>> = EntityItemByToken<CC, ET> & EntityKey<CC>;
|
|
259
|
+
/**
|
|
260
|
+
* Normalize literals: string | readonly string[] -\> union of strings.
|
|
261
|
+
*/
|
|
262
|
+
type KeysFrom<K> = K extends readonly (infer E)[] ? Extract<E, string> : K extends string ? K : never;
|
|
263
|
+
/**
|
|
264
|
+
* Project item shape by keys; if K is never/unknown, fall back to T.
|
|
265
|
+
*/
|
|
266
|
+
type Projected<T, K> = [KeysFrom<K>] extends [never] ? T : T extends object ? Pick<T, Extract<KeysFrom<K>, keyof Exactify<T>>> : T;
|
|
267
|
+
/**
|
|
268
|
+
* Projected item by token — narrows EntityItemByToken by K when provided.
|
|
269
|
+
*/
|
|
270
|
+
type ProjectedItemByToken<CC extends BaseConfigMap, ET extends EntityToken<CC>, K = unknown> = Projected<EntityItemByToken<CC, ET>, K>;
|
|
259
271
|
|
|
260
272
|
/**
|
|
261
273
|
* A result returned by a {@link ShardQueryFunction | `ShardQueryFunction`} querying an individual shard.
|
|
@@ -264,15 +276,16 @@ type EntityRecordByToken<CC extends BaseConfigMap, ET extends EntityToken<CC>> =
|
|
|
264
276
|
* @typeParam ET - Entity token narrowing the item type.
|
|
265
277
|
* @typeParam IT - Index token (for page key typing).
|
|
266
278
|
* @typeParam CF - Optional values-first config literal type for narrowing.
|
|
279
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
267
280
|
*
|
|
268
281
|
* @category EntityManager
|
|
269
282
|
* @protected
|
|
270
283
|
*/
|
|
271
|
-
interface ShardQueryResult<CC extends BaseConfigMap, ET extends EntityToken<CC>, IT extends string, CF = unknown> {
|
|
284
|
+
interface ShardQueryResult<CC extends BaseConfigMap, ET extends EntityToken<CC>, IT extends string, CF = unknown, K = unknown> {
|
|
272
285
|
/** The number of records returned. */
|
|
273
286
|
count: number;
|
|
274
287
|
/** The returned records. */
|
|
275
|
-
items:
|
|
288
|
+
items: ProjectedItemByToken<CC, ET, K>[];
|
|
276
289
|
/** The page key for the next query on this shard. */
|
|
277
290
|
pageKey?: PageKeyByIndex<CC, ET, IT, CF>;
|
|
278
291
|
}
|
|
@@ -290,13 +303,14 @@ interface ShardQueryResult<CC extends BaseConfigMap, ET extends EntityToken<CC>,
|
|
|
290
303
|
* @typeParam ET - Entity token narrowing the item/record types.
|
|
291
304
|
* @typeParam IT - Index token (inferred from shardQueryMap keys).
|
|
292
305
|
* @typeParam CF - Optional values-first config literal type for narrowing.
|
|
306
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
293
307
|
*
|
|
294
308
|
* @category EntityManager
|
|
295
309
|
* @protected
|
|
296
310
|
*/
|
|
297
|
-
type ShardQueryFunction<CC extends BaseConfigMap, ET extends EntityToken<CC>, IT extends string, CF = unknown> = CF extends {
|
|
311
|
+
type ShardQueryFunction<CC extends BaseConfigMap, ET extends EntityToken<CC>, IT extends string, CF = unknown, K = unknown> = CF extends {
|
|
298
312
|
indexes?: infer I;
|
|
299
|
-
} ? I extends Record<string, unknown> ? IT extends keyof I & string ? (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF>> : never : (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF>> : (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF>>;
|
|
313
|
+
} ? I extends Record<string, unknown> ? IT extends keyof I & string ? (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF, K>> : never : (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF, K>> : (hashKey: string, pageKey?: PageKeyByIndex<CC, ET, IT, CF>, pageSize?: number) => Promise<ShardQueryResult<CC, ET, IT, CF, K>>;
|
|
300
314
|
|
|
301
315
|
/**
|
|
302
316
|
* Relates a specific index token to a {@link ShardQueryFunction | `ShardQueryFunction`} to be performed on that index.
|
|
@@ -309,13 +323,14 @@ type ShardQueryFunction<CC extends BaseConfigMap, ET extends EntityToken<CC>, IT
|
|
|
309
323
|
* literal keys (prefer `as const` at call sites), the map keys
|
|
310
324
|
* are constrained to that set. Excess keys are rejected by
|
|
311
325
|
* excess property checks on object literals.
|
|
326
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
312
327
|
*
|
|
313
328
|
* @category EntityManager
|
|
314
329
|
* @protected
|
|
315
330
|
*/
|
|
316
|
-
type ShardQueryMap<CC extends BaseConfigMap, ET extends EntityToken<CC>, ITS extends string, CF = unknown> = CF extends {
|
|
331
|
+
type ShardQueryMap<CC extends BaseConfigMap, ET extends EntityToken<CC>, ITS extends string, CF = unknown, K = unknown> = CF extends {
|
|
317
332
|
indexes?: infer I;
|
|
318
|
-
} ? I extends Record<string, unknown> ? Record<ITS & (keyof I & string), ShardQueryFunction<CC, ET, ITS & (keyof I & string), CF>> : Record<ITS, ShardQueryFunction<CC, ET, ITS, CF>> : Record<ITS, ShardQueryFunction<CC, ET, ITS, CF>>;
|
|
333
|
+
} ? I extends Record<string, unknown> ? Record<ITS & (keyof I & string), ShardQueryFunction<CC, ET, ITS & (keyof I & string), CF, K>> : Record<ITS, ShardQueryFunction<CC, ET, ITS, CF, K>> : Record<ITS, ShardQueryFunction<CC, ET, ITS, CF, K>>;
|
|
319
334
|
/**
|
|
320
335
|
* Convenience alias for ShardQueryMap that derives ITS (index token subset)
|
|
321
336
|
* from a values-first captured config CC (e.g., your config literal type).
|
|
@@ -328,7 +343,7 @@ type ShardQueryMap<CC extends BaseConfigMap, ET extends EntityToken<CC>, ITS ext
|
|
|
328
343
|
*
|
|
329
344
|
* This is optional DX sugar; it does not change runtime behavior.
|
|
330
345
|
*/
|
|
331
|
-
type ShardQueryMapByCC<CC extends BaseConfigMap, ET extends EntityToken<CC>, CCLit = unknown> = ShardQueryMap<CC, ET, IndexTokensFrom<CCLit>, CCLit>;
|
|
346
|
+
type ShardQueryMapByCC<CC extends BaseConfigMap, ET extends EntityToken<CC>, CCLit = unknown, K = unknown> = ShardQueryMap<CC, ET, IndexTokensFrom<CCLit>, CCLit, K>;
|
|
332
347
|
/**
|
|
333
348
|
* Convenience alias for ShardQueryMap that derives ITS (index token subset)
|
|
334
349
|
* directly from a values-first config literal CF when it carries `indexes`.
|
|
@@ -338,7 +353,7 @@ type ShardQueryMapByCC<CC extends BaseConfigMap, ET extends EntityToken<CC>, CCL
|
|
|
338
353
|
*
|
|
339
354
|
* This is optional DX sugar; it does not change runtime behavior.
|
|
340
355
|
*/
|
|
341
|
-
type ShardQueryMapByCF<CC extends BaseConfigMap, ET extends EntityToken<CC>, CF = unknown> = ShardQueryMap<CC, ET, IndexTokensOf<CF>, CF>;
|
|
356
|
+
type ShardQueryMapByCF<CC extends BaseConfigMap, ET extends EntityToken<CC>, CF = unknown, K = unknown> = ShardQueryMap<CC, ET, IndexTokensOf<CF>, CF, K>;
|
|
342
357
|
|
|
343
358
|
/**
|
|
344
359
|
* Options passed to the {@link EntityManager.query | `EntityManager.query`} method.
|
|
@@ -347,11 +362,12 @@ type ShardQueryMapByCF<CC extends BaseConfigMap, ET extends EntityToken<CC>, CF
|
|
|
347
362
|
* @typeParam ET - Entity token narrowing the item types.
|
|
348
363
|
* @typeParam ITS - Index token subset (inferred from shardQueryMap keys).
|
|
349
364
|
* @typeParam CF - Optional values-first config literal type used for index-aware narrowing.
|
|
365
|
+
* @typeParam K - Optional projection keys; narrows item/sort shapes when provided.
|
|
350
366
|
*
|
|
351
367
|
* @category EntityManager
|
|
352
368
|
* @protected
|
|
353
369
|
*/
|
|
354
|
-
interface QueryOptions<CC extends BaseConfigMap, ET extends EntityToken<CC> = EntityToken<CC>, ITS extends string = string, CF = unknown> {
|
|
370
|
+
interface QueryOptions<CC extends BaseConfigMap, ET extends EntityToken<CC> = EntityToken<CC>, ITS extends string = string, CF = unknown, K = unknown> {
|
|
355
371
|
/** Identifies the entity to be queried. Key of {@link Config | `Config`} `entities`. */
|
|
356
372
|
entityToken: ET;
|
|
357
373
|
/**
|
|
@@ -389,11 +405,11 @@ interface QueryOptions<CC extends BaseConfigMap, ET extends EntityToken<CC> = En
|
|
|
389
405
|
* page key, e.g. to match the same string against `firstName` and `lastName`
|
|
390
406
|
* properties without performing a table scan for either.
|
|
391
407
|
*/
|
|
392
|
-
shardQueryMap: ShardQueryMap<CC, ET, ITS, CF>;
|
|
408
|
+
shardQueryMap: ShardQueryMap<CC, ET, ITS, CF, K>;
|
|
393
409
|
/**
|
|
394
|
-
* A {@link SortOrder | `SortOrder`} object specifying the sort order of the result set. Defaults to `[]`.
|
|
410
|
+
* A {@link SortOrder | `SortOrder`} object specifying the sort order of the result set. Defaults to `[]`. Aligned with the projected item shape when K is provided.
|
|
395
411
|
*/
|
|
396
|
-
sortOrder?: SortOrder<
|
|
412
|
+
sortOrder?: SortOrder<ProjectedItemByToken<CC, ET, K>>;
|
|
397
413
|
/**
|
|
398
414
|
* Lower limit to query shard space.
|
|
399
415
|
*
|
|
@@ -430,7 +446,7 @@ interface QueryOptions<CC extends BaseConfigMap, ET extends EntityToken<CC> = En
|
|
|
430
446
|
*
|
|
431
447
|
* This is optional DX sugar; it does not change runtime behavior.
|
|
432
448
|
*/
|
|
433
|
-
type QueryOptionsByCF<CC extends BaseConfigMap, ET extends EntityToken<CC> = EntityToken<CC>, CF = unknown> = QueryOptions<CC, ET, IndexTokensOf<CF>, CF>;
|
|
449
|
+
type QueryOptionsByCF<CC extends BaseConfigMap, ET extends EntityToken<CC> = EntityToken<CC>, CF = unknown, K = unknown> = QueryOptions<CC, ET, IndexTokensOf<CF>, CF, K>;
|
|
434
450
|
/**
|
|
435
451
|
* Convenience alias for QueryOptions that derives ITS (index token subset)
|
|
436
452
|
* from a values-first captured config CC (e.g., your config literal type).
|
|
@@ -443,7 +459,7 @@ type QueryOptionsByCF<CC extends BaseConfigMap, ET extends EntityToken<CC> = Ent
|
|
|
443
459
|
*
|
|
444
460
|
* This is optional DX sugar; it does not change runtime behavior.
|
|
445
461
|
*/
|
|
446
|
-
type QueryOptionsByCC<CCMap extends BaseConfigMap, ET extends EntityToken<CCMap> = EntityToken<CCMap>, CC = unknown> = QueryOptions<CCMap, ET, IndexTokensFrom<CC>, CC>;
|
|
462
|
+
type QueryOptionsByCC<CCMap extends BaseConfigMap, ET extends EntityToken<CCMap> = EntityToken<CCMap>, CC = unknown, K = unknown> = QueryOptions<CCMap, ET, IndexTokensFrom<CC>, CC, K>;
|
|
447
463
|
|
|
448
464
|
/**
|
|
449
465
|
* A result returned by a query across multiple shards, where each shard may receive multiple page queries via a dynamically-generated {@link ShardQueryFunction | `ShardQueryFunction`}.
|
|
@@ -451,15 +467,16 @@ type QueryOptionsByCC<CCMap extends BaseConfigMap, ET extends EntityToken<CCMap>
|
|
|
451
467
|
* @typeParam CC - {@link ConfigMap | `ConfigMap`}.
|
|
452
468
|
* @typeParam ET - Entity token narrowing the result item type.
|
|
453
469
|
* @typeParam ITS - Index token subset (carried for symmetry; not represented in the shape).
|
|
470
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
454
471
|
*
|
|
455
472
|
* @category EntityManager
|
|
456
473
|
* @protected
|
|
457
474
|
*/
|
|
458
|
-
interface QueryResult<CC extends BaseConfigMap, ET extends EntityToken<CC>, ITS extends string> {
|
|
475
|
+
interface QueryResult<CC extends BaseConfigMap, ET extends EntityToken<CC>, ITS extends string, K = unknown> {
|
|
459
476
|
/** Total number of records returned across all shards. */
|
|
460
477
|
count: number;
|
|
461
478
|
/** The returned records. */
|
|
462
|
-
items:
|
|
479
|
+
items: ProjectedItemByToken<CC, ET, K>[];
|
|
463
480
|
/**
|
|
464
481
|
* A compressed, two-layer map of page keys, used to query the next page of
|
|
465
482
|
* data for a given sort key on each shard of a given hash key.
|
|
@@ -598,7 +615,7 @@ declare class EntityManager<CC extends BaseConfigMap> {
|
|
|
598
615
|
*
|
|
599
616
|
* @protected
|
|
600
617
|
*/
|
|
601
|
-
query<ET extends EntityToken<CC>, ITS extends string, CF = unknown>(options: QueryOptions<CC, ET, ITS, CF>): Promise<QueryResult<CC, ET, ITS>>;
|
|
618
|
+
query<ET extends EntityToken<CC>, ITS extends string, CF = unknown, K = unknown>(options: QueryOptions<CC, ET, ITS, CF, K>): Promise<QueryResult<CC, ET, ITS, K>>;
|
|
602
619
|
}
|
|
603
620
|
|
|
604
621
|
/**
|
|
@@ -783,10 +800,11 @@ type QueryBuilderQueryOptions<CC extends BaseConfigMap, CF = unknown> = Omit<Que
|
|
|
783
800
|
* @typeParam EntityClient - {@link BaseEntityClient | `BaseEntityClient`} derived class instance.
|
|
784
801
|
* @typeParam IndexParams - Database platform-specific, index-specific query parameters.
|
|
785
802
|
* @typeParam CF - Optional values-first config literal type for page key narrowing.
|
|
803
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
786
804
|
*
|
|
787
805
|
* @category QueryBuilder
|
|
788
806
|
*/
|
|
789
|
-
declare abstract class BaseQueryBuilder<CC extends BaseConfigMap, EntityClient extends BaseEntityClient<CC>, IndexParams, ET extends EntityToken<CC> = EntityToken<CC>, ITS extends string = string, CF = unknown> {
|
|
807
|
+
declare abstract class BaseQueryBuilder<CC extends BaseConfigMap, EntityClient extends BaseEntityClient<CC>, IndexParams, ET extends EntityToken<CC> = EntityToken<CC>, ITS extends string = string, CF = unknown, K = unknown> {
|
|
790
808
|
/** {@link BaseEntityClient | `EntityClient`} instance. */
|
|
791
809
|
readonly entityClient: EntityClient;
|
|
792
810
|
/** Entity token. */
|
|
@@ -803,15 +821,15 @@ declare abstract class BaseQueryBuilder<CC extends BaseConfigMap, EntityClient e
|
|
|
803
821
|
readonly indexParamsMap: Record<ITS, IndexParams>;
|
|
804
822
|
/** BaseQueryBuilder constructor. */
|
|
805
823
|
constructor(options: BaseQueryBuilderOptions<CC, EntityClient>);
|
|
806
|
-
protected abstract getShardQueryFunction(indexToken: ITS): ShardQueryFunction<CC, ET, ITS, CF>;
|
|
824
|
+
protected abstract getShardQueryFunction(indexToken: ITS): ShardQueryFunction<CC, ET, ITS, CF, K>;
|
|
807
825
|
/**
|
|
808
826
|
* Builds a {@link ShardQueryMap | `ShardQueryMap`} object.
|
|
809
827
|
*
|
|
810
828
|
* @returns - The {@link ShardQueryMap | `ShardQueryMap`} object.
|
|
811
829
|
*/
|
|
812
|
-
build(): ShardQueryMap<CC, ET, ITS, CF>;
|
|
813
|
-
query(options: QueryBuilderQueryOptions<CC, CF>): Promise<QueryResult<CC, ET, ITS>>;
|
|
830
|
+
build(): ShardQueryMap<CC, ET, ITS, CF, K>;
|
|
831
|
+
query(options: QueryBuilderQueryOptions<CC, CF>): Promise<QueryResult<CC, ET, ITS, K>>;
|
|
814
832
|
}
|
|
815
833
|
|
|
816
834
|
export { BaseEntityClient, BaseQueryBuilder, EntityManager, configSchema, createEntityManager };
|
|
817
|
-
export type { BaseConfigMap, BaseEntityClientOptions, BaseQueryBuilderOptions, CapturedConfigMapFrom, Config, ConfigInput, ConfigMap, EntitiesFromSchema, EntityItem, EntityItemByToken, EntityKey, EntityOfToken, EntityRecord, EntityRecordByToken, EntityToken, HasIndexFor, HashKeyFrom, IndexComponentTokens, IndexHashKeyOf, IndexRangeKeyOf, IndexTokensFrom, IndexTokensOf, PageKey, PageKeyByIndex, ParsedConfig, QueryBuilderQueryOptions, QueryOptions, QueryOptionsByCC, QueryOptionsByCF, QueryResult, RangeKeyFrom, ShardBump, ShardQueryFunction, ShardQueryMap, ShardQueryMapByCC, ShardQueryMapByCF, ShardQueryResult, ShardedKeysFrom, TranscodedPropertiesFrom, UnshardedKeysFrom, ValidateConfigMap };
|
|
835
|
+
export type { BaseConfigMap, BaseEntityClientOptions, BaseQueryBuilderOptions, CapturedConfigMapFrom, Config, ConfigInput, ConfigMap, EntitiesFromSchema, EntityItem, EntityItemByToken, EntityKey, EntityOfToken, EntityRecord, EntityRecordByToken, EntityToken, HasIndexFor, HashKeyFrom, IndexComponentTokens, IndexHashKeyOf, IndexRangeKeyOf, IndexTokensFrom, IndexTokensOf, KeysFrom, PageKey, PageKeyByIndex, ParsedConfig, Projected, ProjectedItemByToken, QueryBuilderQueryOptions, QueryOptions, QueryOptionsByCC, QueryOptionsByCF, QueryResult, RangeKeyFrom, ShardBump, ShardQueryFunction, ShardQueryMap, ShardQueryMapByCC, ShardQueryMapByCF, ShardQueryResult, ShardedKeysFrom, TranscodedPropertiesFrom, UnshardedKeysFrom, ValidateConfigMap };
|
|
@@ -7,6 +7,7 @@ import { mapValues } from 'radash';
|
|
|
7
7
|
* @typeParam EntityClient - {@link BaseEntityClient | `BaseEntityClient`} derived class instance.
|
|
8
8
|
* @typeParam IndexParams - Database platform-specific, index-specific query parameters.
|
|
9
9
|
* @typeParam CF - Optional values-first config literal type for page key narrowing.
|
|
10
|
+
* @typeParam K - Optional projection keys; narrows item shape when provided.
|
|
10
11
|
*
|
|
11
12
|
* @category QueryBuilder
|
|
12
13
|
*/
|
|
@@ -28,7 +28,14 @@ class EntityManager {
|
|
|
28
28
|
*/
|
|
29
29
|
constructor(config, logger = console) {
|
|
30
30
|
_EntityManager_config.set(this, void 0);
|
|
31
|
-
|
|
31
|
+
// Accept a compile-time-only `entitiesSchema` key on values-first configs.
|
|
32
|
+
// We strip it here so the same literal config can be used with either the
|
|
33
|
+
// factory or the direct constructor without tripping Zod's strict parser.
|
|
34
|
+
//
|
|
35
|
+
// This keeps runtime semantics unchanged and avoids widening ParsedConfig.
|
|
36
|
+
const cfgWithOptionalES = config;
|
|
37
|
+
const { entitiesSchema: _ignored, ...configForParse } = cfgWithOptionalES;
|
|
38
|
+
__classPrivateFieldSet(this, _EntityManager_config, configSchema.parse(configForParse), "f");
|
|
32
39
|
this.logger = logger;
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
@@ -26,8 +26,10 @@ const { compressToEncodedURIComponent, decompressFromEncodedURIComponent } = lzS
|
|
|
26
26
|
*/
|
|
27
27
|
async function query(entityManager, options) {
|
|
28
28
|
try {
|
|
29
|
-
// Get defaults.
|
|
30
|
-
const
|
|
29
|
+
// Get defaults (avoid unsafe destructuring on generic access).
|
|
30
|
+
const entityDefaults = entityManager.config.entities[options.entityToken];
|
|
31
|
+
const defaultLimit = entityDefaults.defaultLimit;
|
|
32
|
+
const defaultPageSize = entityDefaults.defaultPageSize;
|
|
31
33
|
// Extract params.
|
|
32
34
|
const { entityToken, limit = defaultLimit, item, pageKeyMap, pageSize = defaultPageSize, shardQueryMap, sortOrder = [], timestampFrom = 0, timestampTo = Date.now(), throttle = entityManager.config.throttle, } = options;
|
|
33
35
|
// Validate params.
|
|
@@ -96,8 +98,12 @@ async function query(entityManager, options) {
|
|
|
96
98
|
break;
|
|
97
99
|
} while (workingResult.items.length < limit);
|
|
98
100
|
// Dedupe & sort working result.
|
|
99
|
-
|
|
101
|
+
// Note: when projecting, callers may omit uniqueProperty/sort keys.
|
|
102
|
+
// We perform operations on the full item shape and then cast back.
|
|
103
|
+
const itemsForOps = workingResult.items;
|
|
104
|
+
const dedupedSorted = sort(unique(itemsForOps, (i) => i[entityManager.config.entities[entityToken]
|
|
100
105
|
.uniqueProperty].toString()), sortOrder);
|
|
106
|
+
workingResult.items = dedupedSorted;
|
|
101
107
|
const result = {
|
|
102
108
|
count: workingResult.items.length,
|
|
103
109
|
items: workingResult.items,
|
package/package.json
CHANGED