@travetto/model-redis 8.0.0-alpha.2 → 8.0.0-alpha.20
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 +1 -1
- package/package.json +6 -5
- package/src/service.ts +228 -112
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ This module provides an [redis](https://redis.io)-based implementation for the [
|
|
|
18
18
|
Supported features:
|
|
19
19
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
20
|
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
21
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/
|
|
21
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
|
|
22
22
|
|
|
23
23
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-redis",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Redis backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"directory": "module/model-redis"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@redis/client": "^
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
29
|
+
"@redis/client": "^6.0.0",
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.18",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.18",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.20"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.23"
|
|
35
36
|
},
|
|
36
37
|
"peerDependenciesMeta": {
|
|
37
38
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { createClient } from '@redis/client';
|
|
2
2
|
|
|
3
|
-
import { JSONUtil, ShutdownManager, type Class
|
|
3
|
+
import { castTo, JSONUtil, ShutdownManager, type Class } from '@travetto/runtime';
|
|
4
4
|
import {
|
|
5
|
-
type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelType, type ModelStorageSupport,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelType, type ModelStorageSupport, NotFoundError,
|
|
6
|
+
ExistsError, type OptionalId, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil,
|
|
7
|
+
type ModelListOptions,
|
|
8
8
|
} from '@travetto/model';
|
|
9
|
+
import {
|
|
10
|
+
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
|
|
11
|
+
type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, isModelIndexedIndex,
|
|
12
|
+
type FullKeyedIndexWithPartialBody, type FullKeyedIndexBody, ModelIndexedComputedIndex,
|
|
13
|
+
warnIfIndexedUniqueIndex, warnIfNonIndexedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType,
|
|
14
|
+
} from '@travetto/model-indexed';
|
|
15
|
+
|
|
9
16
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
10
17
|
|
|
11
18
|
import type { RedisModelConfig } from './config.ts';
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
const TERMINATOR = '\xff';
|
|
21
|
+
|
|
22
|
+
type RedisScan = ({ key: string } | { match: string } | { prefix: string }) & { reverse?: boolean };
|
|
14
23
|
type RedisClient = ReturnType<typeof createClient>;
|
|
15
24
|
type RedisMulti = ReturnType<RedisClient['multi']>;
|
|
25
|
+
type ScanState = { cursor?: string, ids: string[] };
|
|
26
|
+
type ScanOp = 'scan' | 'sScan' | 'zRange';
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* A model service backed by redis
|
|
@@ -40,55 +51,114 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
40
51
|
return key;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
async
|
|
44
|
-
let previousCursor = '0';
|
|
45
|
-
let done = false;
|
|
46
|
-
|
|
47
|
-
const flags = { COUNT: count, ...('match' in search ? { MATCH: search.match } : {}) };
|
|
54
|
+
async #scan(operation: ScanOp, cursor: string, search: RedisScan, count = 100): Promise<ScanState> {
|
|
48
55
|
const key = 'key' in search ? search.key : '';
|
|
56
|
+
const flags = { COUNT: count, ...('match' in search ? { MATCH: search.match } : {}) };
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
+
let output: ScanState;
|
|
59
|
+
switch (operation) {
|
|
60
|
+
case 'scan': output = await this.client.scan(cursor ?? '0', flags).then(result => ({ cursor: result.cursor, ids: result.keys })); break;
|
|
61
|
+
case 'sScan': output = await this.client.sScan(key!, cursor ?? '0', flags).then(result => ({ cursor: result.cursor, ids: result.members })); break;
|
|
62
|
+
case 'zRange': {
|
|
63
|
+
const offset = cursor ? +cursor : 0;
|
|
64
|
+
const prefix = 'prefix' in search ? search.prefix : undefined;
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
let bounds: [number | string, number | string];
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
if (prefix !== undefined) {
|
|
69
|
+
bounds = [prefix ? `[${prefix}` : '-', `(${prefix}${TERMINATOR}`];
|
|
70
|
+
} else {
|
|
71
|
+
bounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
72
|
+
}
|
|
73
|
+
if (search.reverse) {
|
|
74
|
+
bounds = [bounds[1], bounds[0]];
|
|
75
|
+
}
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const result = await this.client.zRange(key!, ...bounds, { BY: prefix ? 'LEX' : 'SCORE', REV: search.reverse, LIMIT: { offset, count } });
|
|
78
|
+
output = { cursor: result.length ? (offset + result.length).toString() : undefined, ids: result };
|
|
79
|
+
break;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
82
|
+
return { ...output, cursor: output.cursor === '0' ? undefined : output.cursor };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async * #streamValues(
|
|
86
|
+
operation: ScanOp,
|
|
87
|
+
search: RedisScan,
|
|
88
|
+
options?: ModelPageOptions & ModelListOptions
|
|
89
|
+
): AsyncIterable<ScanState> {
|
|
90
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
91
|
+
let matched: ScanState = { cursor: options?.offset, ids: [] };
|
|
92
|
+
let produced = 0;
|
|
93
|
+
const batchSize = options?.batchSizeHint ?? 100;
|
|
94
|
+
|
|
95
|
+
do {
|
|
96
|
+
const remaining = limit - produced;
|
|
97
|
+
matched = await this.#scan(operation, matched.cursor!, search, Math.min(remaining, batchSize));
|
|
98
|
+
if (matched.ids.length) {
|
|
99
|
+
yield matched;
|
|
100
|
+
produced += matched.ids.length;
|
|
101
|
+
}
|
|
102
|
+
} while (matched.cursor && produced < limit && !(options?.abort?.aborted));
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
#
|
|
70
|
-
|
|
105
|
+
#scanIndex<T extends ModelType>(
|
|
106
|
+
cls: Class<T>,
|
|
107
|
+
idx: SortedIndex<T>,
|
|
108
|
+
body: KeyedIndexBody<T>,
|
|
109
|
+
options?: ModelPageOptions & { prefix?: string }
|
|
110
|
+
): AsyncIterable<ScanState> {
|
|
111
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
112
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
113
|
+
const fullKey = this.#resolveKey(cls, idx.name, computed.getKey());
|
|
114
|
+
switch (idx.type) {
|
|
115
|
+
// case 'indexed:keyed': return this.#streamValues('sScan', { key: fullKey }, options);
|
|
116
|
+
case 'indexed:sorted': {
|
|
117
|
+
return this.#streamValues('zRange', { key: fullKey, prefix: options?.prefix }, options);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async #getBodies<T extends ModelType>(cls: Class<T>, ids: string[], transform: (id: string) => string): Promise<T[]> {
|
|
123
|
+
if (ids.length === 0) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const bodies = (await this.client.mGet(ids.map(transform)))
|
|
127
|
+
.filter((result): result is string => !!result);
|
|
128
|
+
return ModelCrudUtil.filterOutNotFound(bodies.map(body => ModelCrudUtil.load(cls, body)));
|
|
71
129
|
}
|
|
72
130
|
|
|
73
131
|
#removeIndices<T extends ModelType>(cls: Class, item: T, multi: RedisMulti): void {
|
|
74
|
-
for (const idx of ModelRegistryIndex.getIndices(cls
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
133
|
+
if (isModelIndexedIndex(idx)) {
|
|
134
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
135
|
+
const resolvedKey = this.#resolveKey(cls, idx.name, computed.getKey());
|
|
136
|
+
switch (idx.type) {
|
|
137
|
+
case 'indexed:keyed': multi.sRem(resolvedKey, item.id); break;
|
|
138
|
+
case 'indexed:sorted': multi.zRem(resolvedKey, item.id); break;
|
|
139
|
+
}
|
|
80
140
|
}
|
|
81
141
|
}
|
|
82
142
|
}
|
|
83
143
|
|
|
84
144
|
#addIndices<T extends ModelType>(cls: Class, item: T, multi: RedisMulti): void {
|
|
85
|
-
for (const idx of ModelRegistryIndex.getIndices(cls
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
146
|
+
if (isModelIndexedIndex(idx)) {
|
|
147
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
148
|
+
const resolvedKey = this.#resolveKey(cls, idx.name, computed.getKey());
|
|
88
149
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
switch (idx.type) {
|
|
151
|
+
case 'indexed:keyed': multi.sAdd(resolvedKey, item.id); break;
|
|
152
|
+
case 'indexed:sorted': {
|
|
153
|
+
const value = computed.getSort();
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
multi.zAdd(resolvedKey, { score: 0, value: `${value}${TERMINATOR}${item.id}` });
|
|
156
|
+
} else {
|
|
157
|
+
multi.zAdd(resolvedKey, { score: computed.getSort(), value: item.id });
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
92
162
|
}
|
|
93
163
|
}
|
|
94
164
|
}
|
|
@@ -96,10 +166,11 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
96
166
|
async #store<T extends ModelType>(cls: Class<T>, item: T, action: 'write' | 'delete'): Promise<void> {
|
|
97
167
|
const key = this.#resolveKey(cls, item.id);
|
|
98
168
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
169
|
+
const indices = ModelRegistryIndex.getIndices(cls);
|
|
99
170
|
const existing = await this.get(cls, item.id).catch(() => undefined);
|
|
100
171
|
|
|
101
172
|
// Store with indices
|
|
102
|
-
if (
|
|
173
|
+
if (indices.length) {
|
|
103
174
|
const multi = this.client.multi();
|
|
104
175
|
if (existing) {
|
|
105
176
|
this.#removeIndices(cls, existing, multi);
|
|
@@ -146,25 +217,47 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
146
217
|
}
|
|
147
218
|
}
|
|
148
219
|
|
|
149
|
-
async #getIdByIndex<
|
|
220
|
+
async #getIdByIndex<
|
|
221
|
+
T extends ModelType,
|
|
222
|
+
K extends KeyedIndexSelection<T>,
|
|
223
|
+
S extends SortedIndexSelection<T>
|
|
224
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
|
|
150
225
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
151
226
|
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const fullKey = this.#resolveKey(cls, idxConfig.name, key);
|
|
227
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
228
|
+
const resolvedKey = this.#resolveKey(cls, idx.name, computed.getKey());
|
|
155
229
|
let id: string | undefined;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
230
|
+
switch (idx.type) {
|
|
231
|
+
case 'indexed:keyed': {
|
|
232
|
+
if (computed.idPart) {
|
|
233
|
+
const all = await this.client.sMembers(resolvedKey);
|
|
234
|
+
if (!all.find(item => item === computed.idPart!.value)) {
|
|
235
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey());
|
|
236
|
+
}
|
|
237
|
+
id = computed.idPart!.value;
|
|
238
|
+
} else {
|
|
239
|
+
id = await this.client.sRandMember(resolvedKey) ?? undefined;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'indexed:sorted': {
|
|
244
|
+
const sort = computed.getSort();
|
|
245
|
+
const result = await this.client.zRangeByScore(resolvedKey, sort, sort);
|
|
246
|
+
if (computed.idPart) {
|
|
247
|
+
if (!result.find(item => item === computed.idPart!.value)) {
|
|
248
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey());
|
|
249
|
+
}
|
|
250
|
+
id = computed.idPart!.value;
|
|
251
|
+
} else {
|
|
252
|
+
id = result[0];
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
163
256
|
}
|
|
164
257
|
if (id) {
|
|
165
258
|
return id;
|
|
166
259
|
}
|
|
167
|
-
throw new NotFoundError(`${cls.name}: ${idx}`,
|
|
260
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
168
261
|
}
|
|
169
262
|
|
|
170
263
|
@PostConstruct()
|
|
@@ -174,14 +267,8 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
174
267
|
await ModelStorageUtil.storageInitialization(this);
|
|
175
268
|
ShutdownManager.signal.addEventListener('abort', () => this.client.close());
|
|
176
269
|
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
case 'unique': {
|
|
180
|
-
console.error('Unique indices are not supported in redis for', { cls: cls.Ⲑid, idx: idx.name });
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
270
|
+
warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
271
|
+
warnIfNonIndexedIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
185
272
|
}
|
|
186
273
|
}
|
|
187
274
|
|
|
@@ -241,25 +328,9 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
241
328
|
await this.#store(cls, where, 'delete');
|
|
242
329
|
}
|
|
243
330
|
|
|
244
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
245
|
-
for await (const ids of this.#
|
|
246
|
-
|
|
247
|
-
if (!ids.length) {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const bodies = (await this.client.mGet(ids))
|
|
252
|
-
.filter((result): result is string => !!result);
|
|
253
|
-
|
|
254
|
-
for (const body of bodies) {
|
|
255
|
-
try {
|
|
256
|
-
yield await ModelCrudUtil.load(cls, body);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
if (!(error instanceof NotFoundError)) {
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
331
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
332
|
+
for await (const { ids } of this.#streamValues('scan', { match: `${this.#resolveKey(cls)}:*` }, options)) {
|
|
333
|
+
yield await this.#getBodies(cls, ids, id => id);
|
|
263
334
|
}
|
|
264
335
|
}
|
|
265
336
|
|
|
@@ -271,14 +342,17 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
271
342
|
|
|
272
343
|
// Storage
|
|
273
344
|
async createStorage(): Promise<void> {
|
|
274
|
-
|
|
345
|
+
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
346
|
+
warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
347
|
+
warnIfNonIndexedIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
348
|
+
}
|
|
275
349
|
}
|
|
276
350
|
|
|
277
351
|
async deleteStorage(): Promise<void> {
|
|
278
352
|
if (!this.config.namespace) {
|
|
279
353
|
await this.client.flushDb();
|
|
280
354
|
} else {
|
|
281
|
-
for await (const ids of this.#
|
|
355
|
+
for await (const { ids } of this.#streamValues('scan', { match: `${this.#resolveKey('')}*` }, { limit: Number.MAX_SAFE_INTEGER })) {
|
|
282
356
|
if (ids.length) {
|
|
283
357
|
await this.client.del(ids);
|
|
284
358
|
}
|
|
@@ -287,7 +361,7 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
287
361
|
}
|
|
288
362
|
|
|
289
363
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
290
|
-
for await (const ids of this.#
|
|
364
|
+
for await (const { ids } of this.#streamValues('scan', { match: `${this.#resolveKey(model)}:*` }, { limit: Number.MAX_SAFE_INTEGER })) {
|
|
291
365
|
if (ids.length) {
|
|
292
366
|
await this.client.del(ids);
|
|
293
367
|
}
|
|
@@ -295,53 +369,95 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
295
369
|
}
|
|
296
370
|
|
|
297
371
|
// Indexed
|
|
298
|
-
|
|
372
|
+
|
|
373
|
+
async getByIndex<
|
|
374
|
+
T extends ModelType,
|
|
375
|
+
K extends KeyedIndexSelection<T>,
|
|
376
|
+
S extends SortedIndexSelection<T>
|
|
377
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
299
378
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
300
379
|
}
|
|
301
380
|
|
|
302
|
-
async deleteByIndex<
|
|
381
|
+
async deleteByIndex<
|
|
382
|
+
T extends ModelType,
|
|
383
|
+
K extends KeyedIndexSelection<T>,
|
|
384
|
+
S extends SortedIndexSelection<T>
|
|
385
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
303
386
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
304
387
|
}
|
|
305
388
|
|
|
306
|
-
upsertByIndex<
|
|
389
|
+
upsertByIndex<
|
|
390
|
+
T extends ModelType,
|
|
391
|
+
K extends KeyedIndexSelection<T>,
|
|
392
|
+
S extends SortedIndexSelection<T>
|
|
393
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
307
394
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
308
395
|
}
|
|
309
396
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
397
|
+
updateByIndex<
|
|
398
|
+
T extends ModelType,
|
|
399
|
+
K extends KeyedIndexSelection<T>,
|
|
400
|
+
S extends SortedIndexSelection<T>
|
|
401
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
402
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
403
|
+
}
|
|
316
404
|
|
|
317
|
-
|
|
318
|
-
|
|
405
|
+
async updatePartialByIndex<
|
|
406
|
+
T extends ModelType,
|
|
407
|
+
K extends KeyedIndexSelection<T>,
|
|
408
|
+
S extends SortedIndexSelection<T>
|
|
409
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
410
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
411
|
+
return this.update(cls, item);
|
|
412
|
+
}
|
|
319
413
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
414
|
+
async pageByIndex<
|
|
415
|
+
T extends ModelType,
|
|
416
|
+
K extends KeyedIndexSelection<T>,
|
|
417
|
+
S extends SortedIndexSelection<T>
|
|
418
|
+
>(
|
|
419
|
+
cls: Class<T>,
|
|
420
|
+
idx: SortedIndex<T, K, S>,
|
|
421
|
+
body: KeyedIndexBody<T, K>,
|
|
422
|
+
options?: ModelPageOptions
|
|
423
|
+
): Promise<ModelPageResult<T>> {
|
|
424
|
+
const items: T[] = [];
|
|
425
|
+
let lastCursor: string | undefined;
|
|
426
|
+
for await (const { ids, cursor } of this.#scanIndex(cls, idx, body, { limit: 100, ...options })) {
|
|
427
|
+
items.push(...await this.#getBodies(cls, ids, id => this.#resolveKey(cls, id)));
|
|
428
|
+
lastCursor = cursor;
|
|
324
429
|
}
|
|
430
|
+
return { items, nextOffset: lastCursor };
|
|
431
|
+
}
|
|
325
432
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
433
|
+
async * listByIndex<
|
|
434
|
+
T extends ModelType,
|
|
435
|
+
K extends KeyedIndexSelection<T>,
|
|
436
|
+
S extends SortedIndexSelection<T>
|
|
437
|
+
>(
|
|
438
|
+
cls: Class<T>,
|
|
439
|
+
idx: SortedIndex<T, K, S>,
|
|
440
|
+
body: KeyedIndexBody<T, K>,
|
|
441
|
+
options?: ModelListOptions
|
|
442
|
+
): AsyncIterable<T[]> {
|
|
443
|
+
for await (const { ids } of this.#scanIndex(cls, idx, body, options)) {
|
|
444
|
+
yield await this.#getBodies(cls, ids, id => this.#resolveKey(cls, id));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
330
447
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
448
|
+
async suggestByIndex<
|
|
449
|
+
T extends ModelType,
|
|
450
|
+
S extends SortedIndexSelection<T>,
|
|
451
|
+
K extends KeyedIndexSelection<T>,
|
|
452
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
453
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
454
|
+
|
|
455
|
+
const items: T[] = [];
|
|
456
|
+
for await (const { ids } of this.#scanIndex(cls, idx, body, { limit: 10, ...options, prefix })) {
|
|
457
|
+
const cleaned = ids.map(id => id.split(TERMINATOR).at(-1)!);
|
|
458
|
+
items.push(...await this.#getBodies(cls, cleaned, id => this.#resolveKey(cls, id)));
|
|
345
459
|
}
|
|
460
|
+
|
|
461
|
+
return items;
|
|
346
462
|
}
|
|
347
463
|
}
|