@travetto/model 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -33
- package/bin/candidate.ts +1 -1
- package/bin/cli-model_export.ts +7 -3
- package/bin/cli-model_install.ts +3 -3
- package/bin/lib/base-cli-plugin.ts +7 -6
- package/bin/lib/candidate.ts +9 -6
- package/bin/lib/export.ts +1 -1
- package/bin/lib/install.ts +1 -1
- package/package.json +6 -6
- package/src/internal/service/bulk.ts +9 -1
- package/src/internal/service/common.ts +7 -0
- package/src/internal/service/crud.ts +14 -7
- package/src/internal/service/expiry.ts +11 -8
- package/src/internal/service/indexed.ts +28 -8
- package/src/internal/service/storage.ts +3 -2
- package/src/provider/file.ts +32 -30
- package/src/provider/memory.ts +52 -44
- package/src/registry/decorator.ts +3 -2
- package/src/registry/model.ts +15 -14
- package/src/service/basic.ts +1 -1
- package/src/service/bulk.ts +5 -5
- package/src/service/indexed.ts +1 -1
- package/src/service/stream.ts +4 -2
- package/test-support/base.ts +5 -5
- package/test-support/polymorphism.ts +9 -9
- package/test-support/stream.ts +2 -1
- package/test-support/suite.ts +1 -1
|
@@ -15,6 +15,9 @@ type ComputeConfig = {
|
|
|
15
15
|
emptySortValue?: unknown;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
|
|
19
|
+
type IndexSortPart = { path: string[], dir: number, value: number | Date };
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Utils for working with indexed model services
|
|
20
23
|
*/
|
|
@@ -26,34 +29,41 @@ export class ModelIndexedUtil {
|
|
|
26
29
|
* @param idx Index config
|
|
27
30
|
* @param item Item to read values from
|
|
28
31
|
*/
|
|
29
|
-
static computeIndexParts<T extends ModelType>(
|
|
32
|
+
static computeIndexParts<T extends ModelType>(
|
|
33
|
+
cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
|
|
34
|
+
): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
|
|
30
35
|
const cfg = typeof idx === 'string' ? ModelRegistry.getIndex(cls, idx) : idx;
|
|
31
36
|
const sortField = cfg.type === 'sorted' ? cfg.fields[cfg.fields.length - 1] : undefined;
|
|
32
37
|
|
|
33
|
-
const fields:
|
|
38
|
+
const fields: IndexFieldPart[] = [];
|
|
34
39
|
let sortDir: number = 0;
|
|
35
|
-
let sorted:
|
|
40
|
+
let sorted: IndexSortPart | undefined;
|
|
36
41
|
|
|
37
42
|
for (const field of cfg.fields) {
|
|
38
|
-
let f
|
|
39
|
-
let o
|
|
43
|
+
let f: Record<string, unknown> = field;
|
|
44
|
+
let o: Record<string, unknown> = item;
|
|
40
45
|
const parts = [];
|
|
41
46
|
|
|
42
47
|
while (o !== undefined && o !== null) {
|
|
43
48
|
const k = Object.keys(f)[0];
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
44
50
|
o = (o[k] as Record<string, unknown>);
|
|
45
51
|
parts.push(k);
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
46
53
|
const fk = k as (keyof typeof f);
|
|
47
54
|
if (typeof f[fk] === 'boolean' || typeof f[fk] === 'number') {
|
|
48
55
|
if (cfg.type === 'sorted') {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
49
57
|
sortDir = f[fk] === true ? 1 : f[fk] as number;
|
|
50
58
|
}
|
|
51
59
|
break; // At the bottom
|
|
52
60
|
} else {
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
53
62
|
f = f[fk] as Record<string, unknown>;
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
if (field === sortField) {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
57
67
|
sorted = { path: parts, dir: sortDir, value: o as unknown as number | Date };
|
|
58
68
|
}
|
|
59
69
|
if (o === undefined || o === null) {
|
|
@@ -61,9 +71,11 @@ export class ModelIndexedUtil {
|
|
|
61
71
|
if (empty === undefined || empty === Error) {
|
|
62
72
|
throw new IndexNotSupported(cls, cfg, `Missing field value for ${parts.join('.')}`);
|
|
63
73
|
}
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
64
75
|
o = empty as Record<string, unknown>;
|
|
65
76
|
} else {
|
|
66
77
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
67
79
|
fields.push({ path: parts, value: o as unknown as string | boolean | Date | number });
|
|
68
80
|
}
|
|
69
81
|
}
|
|
@@ -78,13 +90,14 @@ export class ModelIndexedUtil {
|
|
|
78
90
|
* @param cls Type to get index for
|
|
79
91
|
* @param idx Index config
|
|
80
92
|
*/
|
|
81
|
-
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, cfg?: ComputeConfig) {
|
|
82
|
-
const res
|
|
93
|
+
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, cfg?: ComputeConfig): Record<string, unknown> {
|
|
94
|
+
const res: Record<string, unknown> = {};
|
|
83
95
|
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, cfg).fields) {
|
|
84
96
|
let sub = res;
|
|
85
97
|
const all = path.slice(0);
|
|
86
98
|
const last = all.pop()!;
|
|
87
99
|
for (const k of all) {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
88
101
|
sub = (sub[k] ??= {}) as typeof res;
|
|
89
102
|
}
|
|
90
103
|
sub[last] = value;
|
|
@@ -98,7 +111,12 @@ export class ModelIndexedUtil {
|
|
|
98
111
|
* @param idx Index config
|
|
99
112
|
* @param item item to process
|
|
100
113
|
*/
|
|
101
|
-
static computeIndexKey<T extends ModelType>(
|
|
114
|
+
static computeIndexKey<T extends ModelType>(
|
|
115
|
+
cls: Class<T>,
|
|
116
|
+
idx: IndexConfig<T> | string,
|
|
117
|
+
item: DeepPartial<T> = {},
|
|
118
|
+
opts?: ComputeConfig & { sep?: string }
|
|
119
|
+
): { type: string, key: string, sort?: number | Date } {
|
|
102
120
|
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(opts ?? {}), includeSortInFields: false });
|
|
103
121
|
const key = fields.map(({ value }) => value).map(x => `${x}`).join(opts?.sep ?? 'ᚕ');
|
|
104
122
|
const cfg = typeof idx === 'string' ? ModelRegistry.getIndex(cls, idx) : idx;
|
|
@@ -117,8 +135,10 @@ export class ModelIndexedUtil {
|
|
|
117
135
|
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
118
136
|
): Promise<T> {
|
|
119
137
|
try {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
120
139
|
const { id } = await service.getByIndex(cls, idx, body as DeepPartial<T>);
|
|
121
140
|
body.id = id;
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
122
142
|
return await service.update(cls, body as T);
|
|
123
143
|
} catch (err) {
|
|
124
144
|
if (err instanceof NotFoundError) {
|
|
@@ -13,15 +13,16 @@ export class ModelStorageUtil {
|
|
|
13
13
|
/**
|
|
14
14
|
* Register change listener on startup
|
|
15
15
|
*/
|
|
16
|
-
static async registerModelChangeListener(storage: ModelStorageSupport, target?: Class) {
|
|
16
|
+
static async registerModelChangeListener(storage: ModelStorageSupport, target?: Class): Promise<void> {
|
|
17
17
|
if (!EnvUtil.isDynamic() || !(storage?.config?.autoCreate ?? !AppManifest.prod)) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
22
|
target = target ?? storage.constructor as Class<ModelStorageSupport>;
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
const checkType = (cls: Class, enforceBase = true) => {
|
|
25
|
+
const checkType = (cls: Class, enforceBase = true): boolean => {
|
|
25
26
|
if (enforceBase && ModelRegistry.getBaseModel(cls) !== cls) {
|
|
26
27
|
return false;
|
|
27
28
|
}
|
package/src/provider/file.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import { createReadStream } from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import { Readable } from 'stream';
|
|
5
6
|
|
|
6
7
|
import { FsUtil, PathUtil, StreamUtil } from '@travetto/boot';
|
|
7
8
|
import { Class, Util, TimeSpan } from '@travetto/base';
|
|
@@ -34,7 +35,7 @@ export class FileModelConfig {
|
|
|
34
35
|
autoCreate?: boolean;
|
|
35
36
|
cullRate?: number | TimeSpan;
|
|
36
37
|
|
|
37
|
-
async postConstruct() {
|
|
38
|
+
async postConstruct(): Promise<void> {
|
|
38
39
|
if (!this.folder) {
|
|
39
40
|
this.folder = PathUtil.resolveUnix(os.tmpdir(), Util.uuid().substring(0, 10));
|
|
40
41
|
}
|
|
@@ -47,17 +48,17 @@ export class FileModelConfig {
|
|
|
47
48
|
@Injectable()
|
|
48
49
|
export class FileModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport {
|
|
49
50
|
|
|
50
|
-
private static async * scanFolder(folder: string, suffix: string) {
|
|
51
|
+
private static async * scanFolder(folder: string, suffix: string): AsyncGenerator<[id: string, field: string]> {
|
|
51
52
|
for (const sub of await fs.readdir(folder)) {
|
|
52
53
|
for (const file of await fs.readdir(PathUtil.resolveUnix(folder, sub))) {
|
|
53
54
|
if (file.endsWith(suffix)) {
|
|
54
|
-
yield [file.replace(suffix, ''), PathUtil.resolveUnix(folder, sub, file)]
|
|
55
|
+
yield [file.replace(suffix, ''), PathUtil.resolveUnix(folder, sub, file)];
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
get client() {
|
|
61
|
+
get client(): string {
|
|
61
62
|
return this.config.folder;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -68,7 +69,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
68
69
|
*/
|
|
69
70
|
constructor(public readonly config: FileModelConfig) { }
|
|
70
71
|
|
|
71
|
-
async #resolveName<T extends ModelType>(cls: Class<T> | string, suffix?: Suffix, id?: string) {
|
|
72
|
+
async #resolveName<T extends ModelType>(cls: Class<T> | string, suffix?: Suffix, id?: string): Promise<string> {
|
|
72
73
|
const name = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
73
74
|
let resolved = PathUtil.resolveUnix(this.config.folder, this.config.namespace, name);
|
|
74
75
|
if (id) {
|
|
@@ -85,7 +86,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
85
86
|
return resolved;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
async #find<T extends ModelType>(cls: Class<T> | string, suffix: Suffix, id?: string) {
|
|
89
|
+
async #find<T extends ModelType>(cls: Class<T> | string, suffix: Suffix, id?: string): Promise<string> {
|
|
89
90
|
const file = await this.#resolveName(cls, suffix, id);
|
|
90
91
|
if (id && !(await FsUtil.exists(file))) {
|
|
91
92
|
throw new NotFoundError(cls, id);
|
|
@@ -93,11 +94,11 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
93
94
|
return file;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
postConstruct() {
|
|
97
|
+
postConstruct(): void {
|
|
97
98
|
ModelExpiryUtil.registerCull(this);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
checkExpiry<T extends ModelType>(cls: Class<T>, item: T) {
|
|
101
|
+
checkExpiry<T extends ModelType>(cls: Class<T>, item: T): T {
|
|
101
102
|
const { expiresAt } = ModelRegistry.get(cls);
|
|
102
103
|
if (expiresAt && ModelExpiryUtil.getExpiryState(cls, item).expired) {
|
|
103
104
|
throw new NotFoundError(cls, item.id);
|
|
@@ -105,11 +106,11 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
105
106
|
return item;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
uuid() {
|
|
109
|
+
uuid(): string {
|
|
109
110
|
return Util.uuid(32);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
async get<T extends ModelType>(cls: Class<T>, id: string) {
|
|
113
|
+
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
113
114
|
await this.#find(cls, '.json', id);
|
|
114
115
|
|
|
115
116
|
const file = await this.#resolveName(cls, '.json', id);
|
|
@@ -122,7 +123,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
122
123
|
throw new NotFoundError(cls, id);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
126
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
126
127
|
if (!item.id) {
|
|
127
128
|
item.id = this.uuid();
|
|
128
129
|
}
|
|
@@ -136,12 +137,12 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
136
137
|
return await this.upsert(cls, item);
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
async update<T extends ModelType>(cls: Class<T>, item: T) {
|
|
140
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
140
141
|
await this.get(cls, item.id);
|
|
141
142
|
return await this.upsert(cls, item);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
145
|
+
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
145
146
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
146
147
|
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
147
148
|
|
|
@@ -151,35 +152,36 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
151
152
|
return prepped;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
155
|
+
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
155
156
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
156
157
|
const id = item.id;
|
|
157
158
|
item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
158
159
|
const file = await this.#resolveName(cls, '.json', item.id);
|
|
159
160
|
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
160
161
|
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
161
163
|
return item as T;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
166
|
+
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
165
167
|
const file = await this.#find(cls, '.json', id);
|
|
166
168
|
await fs.unlink(file);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
async * list<T extends ModelType>(cls: Class<T>) {
|
|
171
|
+
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
170
172
|
for await (const [id] of FileModelService.scanFolder(await this.#resolveName(cls, '.json'), '.json')) {
|
|
171
173
|
try {
|
|
172
174
|
yield await this.get(cls, id);
|
|
173
|
-
} catch (
|
|
174
|
-
if (!(
|
|
175
|
-
throw
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (!(err instanceof NotFoundError)) {
|
|
177
|
+
throw err;
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
// Stream
|
|
182
|
-
async upsertStream(location: string, input:
|
|
184
|
+
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
183
185
|
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
184
186
|
await Promise.all([
|
|
185
187
|
StreamUtil.writeToFile(input, file),
|
|
@@ -187,19 +189,19 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
187
189
|
]);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
async getStream(location: string) {
|
|
192
|
+
async getStream(location: string): Promise<Readable> {
|
|
191
193
|
const file = await this.#find(STREAMS, BIN, location);
|
|
192
194
|
return createReadStream(file);
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
async describeStream(location: string) {
|
|
197
|
+
async describeStream(location: string): Promise<StreamMeta> {
|
|
196
198
|
const file = await this.#find(STREAMS, META, location);
|
|
197
199
|
const content = await StreamUtil.streamToBuffer(createReadStream(file));
|
|
198
|
-
const text = JSON.parse(content.toString('utf8'));
|
|
199
|
-
return text
|
|
200
|
+
const text: StreamMeta = JSON.parse(content.toString('utf8'));
|
|
201
|
+
return text;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
|
-
async deleteStream(location: string) {
|
|
204
|
+
async deleteStream(location: string): Promise<void> {
|
|
203
205
|
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
204
206
|
if (await FsUtil.exists(file)) {
|
|
205
207
|
await Promise.all([
|
|
@@ -212,7 +214,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Expiry
|
|
215
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>) {
|
|
217
|
+
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
216
218
|
const deleted = [];
|
|
217
219
|
for await (const el of this.list(cls)) {
|
|
218
220
|
if (ModelExpiryUtil.getExpiryState(cls, el).expired) {
|
|
@@ -222,17 +224,17 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
222
224
|
return (await Promise.all(deleted)).length;
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
// Storage
|
|
226
|
-
async createStorage() {
|
|
227
|
+
// Storage management
|
|
228
|
+
async createStorage(): Promise<void> {
|
|
227
229
|
const dir = PathUtil.resolveUnix(this.config.folder, this.config.namespace);
|
|
228
230
|
await fs.mkdir(dir, { recursive: true });
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
async deleteStorage() {
|
|
233
|
+
async deleteStorage(): Promise<void> {
|
|
232
234
|
await FsUtil.unlinkRecursive(PathUtil.resolveUnix(this.config.folder, this.config.namespace));
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
async truncateModel(cls: Class<ModelType>) {
|
|
237
|
+
async truncateModel(cls: Class<ModelType>): Promise<void> {
|
|
236
238
|
await FsUtil.unlinkRecursive(await this.#resolveName(cls === StreamModel ? STREAMS : cls));
|
|
237
239
|
}
|
|
238
240
|
}
|
package/src/provider/memory.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
|
|
1
3
|
import { StreamUtil } from '@travetto/boot';
|
|
2
4
|
import { Util, Class, TimeSpan } from '@travetto/base';
|
|
3
5
|
import { DeepPartial } from '@travetto/schema';
|
|
@@ -22,6 +24,8 @@ import { IndexConfig } from '../registry/types';
|
|
|
22
24
|
|
|
23
25
|
const STREAM_META = `${STREAMS}_meta`;
|
|
24
26
|
|
|
27
|
+
type StoreType = Map<string, Buffer>;
|
|
28
|
+
|
|
25
29
|
@Config('model.memory')
|
|
26
30
|
export class MemoryModelConfig {
|
|
27
31
|
autoCreate?: boolean;
|
|
@@ -29,14 +33,14 @@ export class MemoryModelConfig {
|
|
|
29
33
|
cullRate?: number | TimeSpan;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string) {
|
|
36
|
+
function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string): string {
|
|
33
37
|
return [cls.ᚕid, typeof idx === 'string' ? idx : idx.name, suffix].filter(x => !!x).join(':');
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number) {
|
|
40
|
+
function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
|
|
37
41
|
let id: string | undefined;
|
|
38
42
|
if (data instanceof Set) {
|
|
39
|
-
id = data.values().next().value
|
|
43
|
+
id = data.values().next().value;
|
|
40
44
|
} else {
|
|
41
45
|
id = [...data.entries()].find(([k, v]) => value === undefined || v === value)?.[0];
|
|
42
46
|
}
|
|
@@ -49,16 +53,16 @@ function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | n
|
|
|
49
53
|
@Injectable()
|
|
50
54
|
export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
51
55
|
|
|
52
|
-
#store = new Map<string,
|
|
56
|
+
#store = new Map<string, StoreType>();
|
|
53
57
|
#indices = {
|
|
54
58
|
sorted: new Map<string, Map<string, Map<string, number>>>(),
|
|
55
59
|
unsorted: new Map<string, Map<string, Set<string>>>()
|
|
56
60
|
};
|
|
57
|
-
get client() { return this.#store; }
|
|
61
|
+
get client(): Map<string, StoreType> { return this.#store; }
|
|
58
62
|
|
|
59
63
|
constructor(public readonly config: MemoryModelConfig) { }
|
|
60
64
|
|
|
61
|
-
#getStore<T extends ModelType>(cls: Class<T> | string):
|
|
65
|
+
#getStore<T extends ModelType>(cls: Class<T> | string): StoreType {
|
|
62
66
|
const key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
63
67
|
if (!this.#store.has(key)) {
|
|
64
68
|
this.#store.set(key, new Map());
|
|
@@ -66,7 +70,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
66
70
|
return this.#store.get(key)!;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
#find<T extends ModelType>(cls: Class<T> | string, id?: string, errorState?: 'data' | 'notfound') {
|
|
73
|
+
#find<T extends ModelType>(cls: Class<T> | string, id?: string, errorState?: 'data' | 'notfound'): StoreType {
|
|
70
74
|
const store = this.#getStore(cls);
|
|
71
75
|
|
|
72
76
|
if (id && errorState && (errorState === 'notfound' ? !store.has(id) : store.has(id))) {
|
|
@@ -76,24 +80,26 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
76
80
|
return store;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
async #removeIndices<T extends ModelType>(cls: Class<T>, id: string) {
|
|
83
|
+
async #removeIndices<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
80
84
|
try {
|
|
81
85
|
const item = await this.get(cls, id);
|
|
82
86
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
83
87
|
const idxName = indexName(cls, idx);
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
84
89
|
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
85
90
|
this.#indices[idx.type].get(idxName)?.get(key)?.delete(id);
|
|
86
91
|
}
|
|
87
|
-
} catch (
|
|
88
|
-
if (!(
|
|
89
|
-
throw
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!(err instanceof NotFoundError)) {
|
|
94
|
+
throw err;
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
async #writeIndices<T extends ModelType>(cls: Class<T>, item: T) {
|
|
99
|
+
async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
|
|
95
100
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
96
101
|
const idxName = indexName(cls, idx);
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
97
103
|
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
98
104
|
let index = this.#indices[idx.type].get(idxName)?.get(key);
|
|
99
105
|
|
|
@@ -115,7 +121,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
115
121
|
|
|
116
122
|
async #write<T extends ModelType>(cls: Class<T>, item: T, action: 'remove'): Promise<void>;
|
|
117
123
|
async #write<T extends ModelType>(cls: Class<T>, item: T, action: 'write'): Promise<T>;
|
|
118
|
-
async #write<T extends ModelType>(cls: Class<T>, item: T, action: 'write' | 'remove') {
|
|
124
|
+
async #write<T extends ModelType>(cls: Class<T>, item: T, action: 'write' | 'remove'): Promise<T | void> {
|
|
119
125
|
const store = this.#getStore(cls);
|
|
120
126
|
await this.#removeIndices(cls, item.id);
|
|
121
127
|
if (action === 'write') {
|
|
@@ -145,7 +151,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
145
151
|
throw new NotFoundError(cls, key);
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
async postConstruct() {
|
|
154
|
+
async postConstruct(): Promise<void> {
|
|
149
155
|
await ModelStorageUtil.registerModelChangeListener(this);
|
|
150
156
|
ModelExpiryUtil.registerCull(this);
|
|
151
157
|
|
|
@@ -153,7 +159,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
153
159
|
for (const idx of ModelRegistry.get(el).indices ?? []) {
|
|
154
160
|
switch (idx.type) {
|
|
155
161
|
case 'unique': {
|
|
156
|
-
console.error('Unique
|
|
162
|
+
console.error('Unique indices are not supported for', { cls: el.ᚕid, idx: idx.name });
|
|
157
163
|
break;
|
|
158
164
|
}
|
|
159
165
|
}
|
|
@@ -162,11 +168,11 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
// CRUD Support
|
|
165
|
-
uuid() {
|
|
171
|
+
uuid(): string {
|
|
166
172
|
return Util.uuid(32);
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
async get<T extends ModelType>(cls: Class<T>, id: string) {
|
|
175
|
+
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
170
176
|
const store = this.#getStore(cls);
|
|
171
177
|
if (store.has(id)) {
|
|
172
178
|
const res = await ModelCrudUtil.load(cls, store.get(id)!);
|
|
@@ -183,7 +189,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
183
189
|
throw new NotFoundError(cls, id);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
192
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
187
193
|
if (!item.id) {
|
|
188
194
|
item.id = this.uuid();
|
|
189
195
|
}
|
|
@@ -191,12 +197,12 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
191
197
|
return await this.upsert(cls, item);
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
async update<T extends ModelType>(cls: Class<T>, item: T) {
|
|
200
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
195
201
|
await this.get(cls, item.id);
|
|
196
202
|
return await this.upsert(cls, item);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
205
|
+
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
200
206
|
const store = this.#getStore(cls);
|
|
201
207
|
if (item.id && store.has(item.id)) {
|
|
202
208
|
await ModelCrudUtil.load(cls, store.get(item.id)!, 'exists');
|
|
@@ -205,64 +211,66 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
205
211
|
return await this.#write(cls, prepped, 'write');
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
214
|
+
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
209
215
|
const id = item.id;
|
|
210
216
|
const clean = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
211
217
|
return await this.#write(cls, clean, 'write');
|
|
212
218
|
}
|
|
213
219
|
|
|
214
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
220
|
+
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
215
221
|
const store = this.#getStore(cls);
|
|
216
222
|
if (!store.has(id)) {
|
|
217
223
|
throw new NotFoundError(cls, id);
|
|
218
224
|
}
|
|
219
225
|
await ModelCrudUtil.load(cls, store.get(id)!);
|
|
220
|
-
|
|
226
|
+
const where: ModelType = { id };
|
|
227
|
+
await this.#write(cls, where, 'remove');
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
async * list<T extends ModelType>(cls: Class<T>) {
|
|
230
|
+
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
224
231
|
for (const id of this.#getStore(cls).keys()) {
|
|
225
232
|
try {
|
|
226
233
|
yield await this.get(cls, id);
|
|
227
|
-
} catch (
|
|
228
|
-
if (!(
|
|
229
|
-
throw
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (!(err instanceof NotFoundError)) {
|
|
236
|
+
throw err;
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
// Stream Support
|
|
236
|
-
async upsertStream(location: string, input:
|
|
243
|
+
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
237
244
|
const streams = this.#getStore(STREAMS);
|
|
238
|
-
const
|
|
239
|
-
|
|
245
|
+
const metaContent = this.#getStore(STREAM_META);
|
|
246
|
+
metaContent.set(location, Buffer.from(JSON.stringify(meta)));
|
|
240
247
|
streams.set(location, await StreamUtil.streamToBuffer(input));
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
async getStream(location: string) {
|
|
250
|
+
async getStream(location: string): Promise<Readable> {
|
|
244
251
|
const streams = this.#find(STREAMS, location, 'notfound');
|
|
245
252
|
return StreamUtil.bufferToStream(streams.get(location)!);
|
|
246
253
|
}
|
|
247
254
|
|
|
248
|
-
async describeStream(location: string) {
|
|
249
|
-
const
|
|
250
|
-
|
|
255
|
+
async describeStream(location: string): Promise<StreamMeta> {
|
|
256
|
+
const metaContent = this.#find(STREAM_META, location, 'notfound');
|
|
257
|
+
const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
|
|
258
|
+
return meta;
|
|
251
259
|
}
|
|
252
260
|
|
|
253
|
-
async deleteStream(location: string) {
|
|
261
|
+
async deleteStream(location: string): Promise<void> {
|
|
254
262
|
const streams = this.#getStore(STREAMS);
|
|
255
|
-
const
|
|
263
|
+
const metaContent = this.#getStore(STREAM_META);
|
|
256
264
|
if (streams.has(location)) {
|
|
257
265
|
streams.delete(location);
|
|
258
|
-
|
|
266
|
+
metaContent.delete(location);
|
|
259
267
|
} else {
|
|
260
268
|
throw new NotFoundError('Stream', location);
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
// Expiry Support
|
|
265
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>) {
|
|
273
|
+
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
266
274
|
const deleting = [];
|
|
267
275
|
const store = this.#getStore(cls);
|
|
268
276
|
for (const id of [...store.keys()]) {
|
|
@@ -274,17 +282,17 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
274
282
|
}
|
|
275
283
|
|
|
276
284
|
// Storage Support
|
|
277
|
-
async createStorage() {
|
|
285
|
+
async createStorage(): Promise<void> {
|
|
278
286
|
}
|
|
279
287
|
|
|
280
|
-
async deleteStorage() {
|
|
288
|
+
async deleteStorage(): Promise<void> {
|
|
281
289
|
this.#store.clear();
|
|
282
290
|
this.#indices.sorted.clear();
|
|
283
291
|
this.#indices.unsorted.clear();
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
|
|
287
|
-
async createModel<T extends ModelType>(cls: Class<T>) {
|
|
295
|
+
async createModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
288
296
|
for (const idx of ModelRegistry.get(cls).indices ?? []) {
|
|
289
297
|
if (idx.type === 'sorted' || idx.type === 'unsorted') {
|
|
290
298
|
this.#indices[idx.type].set(indexName(cls, idx), new Map());
|
|
@@ -292,7 +300,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
async truncateModel<T extends ModelType>(cls: Class<T>) {
|
|
303
|
+
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
296
304
|
if (cls === StreamModel) {
|
|
297
305
|
this.#getStore(STREAMS).clear();
|
|
298
306
|
this.#getStore(STREAM_META).clear();
|
|
@@ -306,7 +314,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
306
314
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
307
315
|
}
|
|
308
316
|
|
|
309
|
-
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>) {
|
|
317
|
+
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
|
|
310
318
|
await this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
311
319
|
}
|
|
312
320
|
|
|
@@ -314,7 +322,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
314
322
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
315
323
|
}
|
|
316
324
|
|
|
317
|
-
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>):
|
|
325
|
+
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
318
326
|
const config = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
319
327
|
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body, { emptySortValue: null });
|
|
320
328
|
const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
|
|
@@ -24,7 +24,7 @@ export function Model(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
|
24
24
|
*/
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
26
|
export function Index<T>(...indices: IndexConfig<any>[]) {
|
|
27
|
-
return function (target: Class<T>) {
|
|
27
|
+
return function (target: Class<T>): void {
|
|
28
28
|
ModelRegistry.getOrCreatePending(target).indices!.push(...indices);
|
|
29
29
|
};
|
|
30
30
|
}
|
|
@@ -34,7 +34,8 @@ export function Index<T>(...indices: IndexConfig<any>[]) {
|
|
|
34
34
|
* @augments `@trv:schema/Field`
|
|
35
35
|
*/
|
|
36
36
|
export function ExpiresAt() {
|
|
37
|
-
return <K extends string, T extends Partial<Record<K, Date>>>(tgt: T, prop: K) => {
|
|
37
|
+
return <K extends string, T extends Partial<Record<K, Date>>>(tgt: T, prop: K): void => {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
38
39
|
ModelRegistry.register(tgt.constructor as Class<T>, { expiresAt: prop });
|
|
39
40
|
};
|
|
40
41
|
}
|