@travetto/model 5.0.0-rc.8 → 5.0.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 +80 -146
- package/__index__.ts +1 -4
- package/package.json +7 -7
- package/src/internal/service/blob.ts +47 -0
- package/src/internal/service/common.ts +20 -26
- package/src/internal/service/crud.ts +15 -21
- package/src/internal/service/expiry.ts +2 -5
- package/src/internal/service/indexed.ts +13 -25
- package/src/internal/service/storage.ts +1 -4
- package/src/registry/decorator.ts +7 -12
- package/src/registry/model.ts +5 -6
- package/src/service/blob.ts +36 -0
- package/support/bin/candidate.ts +2 -3
- package/support/doc.support.tsx +2 -2
- package/support/fixtures/alpha.txt +1 -0
- package/support/fixtures/empty +0 -0
- package/support/fixtures/empty.m4a +0 -0
- package/support/fixtures/logo.gif +0 -0
- package/support/fixtures/logo.png +0 -0
- package/support/fixtures/small-audio +0 -0
- package/support/fixtures/small-audio.mp3 +0 -0
- package/support/test/base.ts +3 -3
- package/support/test/blob.ts +136 -0
- package/support/test/crud.ts +3 -3
- package/support/test/polymorphism.ts +7 -6
- package/support/test/suite.ts +5 -5
- package/src/internal/service/stream.ts +0 -22
- package/src/provider/file.ts +0 -233
- package/src/provider/memory.ts +0 -341
- package/src/service/stream.ts +0 -72
- package/support/test/stream.ts +0 -110
package/src/provider/file.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
5
|
-
import { pipeline } from 'node:stream/promises';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
|
|
8
|
-
import { Class, TimeSpan, Runtime } from '@travetto/runtime';
|
|
9
|
-
import { Injectable } from '@travetto/di';
|
|
10
|
-
import { Config } from '@travetto/config';
|
|
11
|
-
import { Required } from '@travetto/schema';
|
|
12
|
-
|
|
13
|
-
import { ModelCrudSupport } from '../service/crud';
|
|
14
|
-
import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
|
|
15
|
-
import { ModelType, OptionalId } from '../types/model';
|
|
16
|
-
import { ModelExpirySupport } from '../service/expiry';
|
|
17
|
-
import { ModelRegistry } from '../registry/model';
|
|
18
|
-
import { ModelStorageSupport } from '../service/storage';
|
|
19
|
-
import { ModelCrudUtil } from '../internal/service/crud';
|
|
20
|
-
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
21
|
-
import { NotFoundError } from '../error/not-found';
|
|
22
|
-
import { ExistsError } from '../error/exists';
|
|
23
|
-
import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
|
|
24
|
-
|
|
25
|
-
type Suffix = '.bin' | '.meta' | '.json' | '.expires';
|
|
26
|
-
|
|
27
|
-
const BIN = '.bin';
|
|
28
|
-
const META = '.meta';
|
|
29
|
-
|
|
30
|
-
@Config('model.file')
|
|
31
|
-
export class FileModelConfig {
|
|
32
|
-
@Required(false)
|
|
33
|
-
folder: string;
|
|
34
|
-
namespace: string = '.';
|
|
35
|
-
autoCreate?: boolean;
|
|
36
|
-
cullRate?: number | TimeSpan;
|
|
37
|
-
|
|
38
|
-
async postConstruct(): Promise<void> {
|
|
39
|
-
this.folder ??= path.resolve(os.tmpdir(), `trv_file_${Runtime.main.name.replace(/[^a-z]/ig, '_')}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const exists = (f: string): Promise<boolean> => fs.stat(f).then(() => true, () => false);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Standard file support
|
|
47
|
-
*/
|
|
48
|
-
@Injectable()
|
|
49
|
-
export class FileModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport {
|
|
50
|
-
|
|
51
|
-
private static async * scanFolder(folder: string, suffix: string): AsyncGenerator<[id: string, field: string]> {
|
|
52
|
-
for (const sub of await fs.readdir(folder)) {
|
|
53
|
-
for (const file of await fs.readdir(path.resolve(folder, sub))) {
|
|
54
|
-
if (file.endsWith(suffix)) {
|
|
55
|
-
yield [file.replace(suffix, ''), path.resolve(folder, sub, file)];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
idSource = ModelCrudUtil.uuidSource();
|
|
62
|
-
|
|
63
|
-
get client(): string {
|
|
64
|
-
return this.config.folder;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The root location for all activity
|
|
69
|
-
*/
|
|
70
|
-
constructor(public readonly config: FileModelConfig) { }
|
|
71
|
-
|
|
72
|
-
async #resolveName<T extends ModelType>(cls: Class<T> | string, suffix?: Suffix, id?: string): Promise<string> {
|
|
73
|
-
const name = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
74
|
-
let resolved = path.resolve(this.config.folder, this.config.namespace, name);
|
|
75
|
-
if (id) {
|
|
76
|
-
resolved = path.resolve(resolved, id.replace(/^[/]/, '').substring(0, 3));
|
|
77
|
-
}
|
|
78
|
-
let dir = resolved;
|
|
79
|
-
if (id) {
|
|
80
|
-
resolved = path.resolve(resolved, `${id}${suffix}`);
|
|
81
|
-
dir = path.dirname(resolved);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await fs.mkdir(dir, { recursive: true });
|
|
85
|
-
return resolved;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async #find<T extends ModelType>(cls: Class<T> | string, suffix: Suffix, id?: string): Promise<string> {
|
|
89
|
-
const file = await this.#resolveName(cls, suffix, id);
|
|
90
|
-
if (id && !(await exists(file))) {
|
|
91
|
-
throw new NotFoundError(cls, id);
|
|
92
|
-
}
|
|
93
|
-
return file;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
postConstruct(): void {
|
|
97
|
-
ModelExpiryUtil.registerCull(this);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
checkExpiry<T extends ModelType>(cls: Class<T>, item: T): T {
|
|
101
|
-
const { expiresAt } = ModelRegistry.get(cls);
|
|
102
|
-
if (expiresAt && ModelExpiryUtil.getExpiryState(cls, item).expired) {
|
|
103
|
-
throw new NotFoundError(cls, item.id);
|
|
104
|
-
}
|
|
105
|
-
return item;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
109
|
-
await this.#find(cls, '.json', id);
|
|
110
|
-
|
|
111
|
-
const file = await this.#resolveName(cls, '.json', id);
|
|
112
|
-
|
|
113
|
-
if (await exists(file)) {
|
|
114
|
-
const content = await fs.readFile(file);
|
|
115
|
-
return this.checkExpiry(cls, await ModelCrudUtil.load(cls, content));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new NotFoundError(cls, id);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
122
|
-
if (!item.id) {
|
|
123
|
-
item.id = this.idSource.create();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const file = await this.#resolveName(cls, '.json', item.id);
|
|
127
|
-
|
|
128
|
-
if (await exists(file)) {
|
|
129
|
-
throw new ExistsError(cls, item.id!);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return await this.upsert(cls, item);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
136
|
-
await this.get(cls, item.id);
|
|
137
|
-
return await this.upsert(cls, item);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
141
|
-
ModelCrudUtil.ensureNotSubType(cls);
|
|
142
|
-
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
143
|
-
|
|
144
|
-
const file = await this.#resolveName(cls, '.json', item.id);
|
|
145
|
-
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
146
|
-
|
|
147
|
-
return prepped;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
151
|
-
ModelCrudUtil.ensureNotSubType(cls);
|
|
152
|
-
const id = item.id;
|
|
153
|
-
item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
154
|
-
const file = await this.#resolveName(cls, '.json', item.id);
|
|
155
|
-
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
156
|
-
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
158
|
-
return item as T;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
162
|
-
const file = await this.#find(cls, '.json', id);
|
|
163
|
-
await fs.unlink(file);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
167
|
-
for await (const [id] of FileModelService.scanFolder(await this.#resolveName(cls, '.json'), '.json')) {
|
|
168
|
-
try {
|
|
169
|
-
yield await this.get(cls, id);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
if (!(err instanceof NotFoundError)) {
|
|
172
|
-
throw err;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Stream
|
|
179
|
-
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
180
|
-
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
181
|
-
await Promise.all([
|
|
182
|
-
await pipeline(input, createWriteStream(file)),
|
|
183
|
-
fs.writeFile(file.replace(BIN, META), JSON.stringify(meta), 'utf8')
|
|
184
|
-
]);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async getStream(location: string, range?: StreamRange): Promise<Readable> {
|
|
188
|
-
const file = await this.#find(STREAMS, BIN, location);
|
|
189
|
-
if (range) {
|
|
190
|
-
const meta = await this.describeStream(location);
|
|
191
|
-
range = enforceRange(range, meta.size);
|
|
192
|
-
}
|
|
193
|
-
return createReadStream(file, range);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async describeStream(location: string): Promise<StreamMeta> {
|
|
197
|
-
const file = await this.#find(STREAMS, META, location);
|
|
198
|
-
const content = await fs.readFile(file);
|
|
199
|
-
const text: StreamMeta = JSON.parse(content.toString('utf8'));
|
|
200
|
-
return text;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async deleteStream(location: string): Promise<void> {
|
|
204
|
-
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
205
|
-
if (await exists(file)) {
|
|
206
|
-
await Promise.all([
|
|
207
|
-
fs.unlink(file),
|
|
208
|
-
fs.unlink(file.replace('.bin', META))
|
|
209
|
-
]);
|
|
210
|
-
} else {
|
|
211
|
-
throw new NotFoundError('Stream', location);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Expiry
|
|
216
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
217
|
-
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Storage management
|
|
221
|
-
async createStorage(): Promise<void> {
|
|
222
|
-
const dir = path.resolve(this.config.folder, this.config.namespace);
|
|
223
|
-
await fs.mkdir(dir, { recursive: true });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async deleteStorage(): Promise<void> {
|
|
227
|
-
await fs.rm(path.resolve(this.config.folder, this.config.namespace), { recursive: true, force: true });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async truncateModel(cls: Class<ModelType>): Promise<void> {
|
|
231
|
-
await fs.rm(await this.#resolveName(cls === StreamModel ? STREAMS : cls), { recursive: true, force: true });
|
|
232
|
-
}
|
|
233
|
-
}
|
package/src/provider/memory.ts
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
3
|
-
|
|
4
|
-
import { Class, TimeSpan, DeepPartial } from '@travetto/runtime';
|
|
5
|
-
import { Injectable } from '@travetto/di';
|
|
6
|
-
import { Config } from '@travetto/config';
|
|
7
|
-
|
|
8
|
-
import { ModelCrudSupport } from '../service/crud';
|
|
9
|
-
import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
|
|
10
|
-
import { ModelType, OptionalId } from '../types/model';
|
|
11
|
-
import { ModelExpirySupport } from '../service/expiry';
|
|
12
|
-
import { ModelRegistry } from '../registry/model';
|
|
13
|
-
import { ModelStorageSupport } from '../service/storage';
|
|
14
|
-
import { ModelCrudUtil } from '../internal/service/crud';
|
|
15
|
-
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
16
|
-
import { NotFoundError } from '../error/not-found';
|
|
17
|
-
import { ExistsError } from '../error/exists';
|
|
18
|
-
import { ModelIndexedSupport } from '../service/indexed';
|
|
19
|
-
import { ModelIndexedUtil } from '../internal/service/indexed';
|
|
20
|
-
import { ModelStorageUtil } from '../internal/service/storage';
|
|
21
|
-
import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
|
|
22
|
-
import { IndexConfig } from '../registry/types';
|
|
23
|
-
|
|
24
|
-
const STREAM_META = `${STREAMS}_meta`;
|
|
25
|
-
|
|
26
|
-
type StoreType = Map<string, Buffer>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@Config('model.memory')
|
|
30
|
-
export class MemoryModelConfig {
|
|
31
|
-
autoCreate?: boolean = true;
|
|
32
|
-
namespace?: string;
|
|
33
|
-
cullRate?: number | TimeSpan;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string): string {
|
|
37
|
-
return [cls.Ⲑid, typeof idx === 'string' ? idx : idx.name, suffix].filter(x => !!x).join(':');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
|
|
41
|
-
let id: string | undefined;
|
|
42
|
-
if (data instanceof Set) {
|
|
43
|
-
id = data.values().next().value;
|
|
44
|
-
} else {
|
|
45
|
-
id = [...data.entries()].find(([k, v]) => value === undefined || v === value)?.[0];
|
|
46
|
-
}
|
|
47
|
-
return id;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Standard in-memory support
|
|
52
|
-
*/
|
|
53
|
-
@Injectable()
|
|
54
|
-
export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
55
|
-
|
|
56
|
-
#store = new Map<string, StoreType>();
|
|
57
|
-
#indices = {
|
|
58
|
-
sorted: new Map<string, Map<string, Map<string, number>>>(),
|
|
59
|
-
unsorted: new Map<string, Map<string, Set<string>>>()
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
idSource = ModelCrudUtil.uuidSource();
|
|
63
|
-
get client(): Map<string, StoreType> { return this.#store; }
|
|
64
|
-
|
|
65
|
-
constructor(public readonly config: MemoryModelConfig) { }
|
|
66
|
-
|
|
67
|
-
#getStore<T extends ModelType>(cls: Class<T> | string): StoreType {
|
|
68
|
-
const key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
69
|
-
if (!this.#store.has(key)) {
|
|
70
|
-
this.#store.set(key, new Map());
|
|
71
|
-
}
|
|
72
|
-
return this.#store.get(key)!;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#find<T extends ModelType>(cls: Class<T> | string, id?: string, errorState?: 'data' | 'notfound'): StoreType {
|
|
76
|
-
const store = this.#getStore(cls);
|
|
77
|
-
|
|
78
|
-
if (id && errorState && (errorState === 'notfound' ? !store.has(id) : store.has(id))) {
|
|
79
|
-
throw errorState === 'notfound' ? new NotFoundError(cls, id) : new ExistsError(cls, id);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return store;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async #removeIndices<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
86
|
-
try {
|
|
87
|
-
const item = await this.get(cls, id);
|
|
88
|
-
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
89
|
-
const idxName = indexName(cls, idx);
|
|
90
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
91
|
-
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
92
|
-
this.#indices[idx.type].get(idxName)?.get(key)?.delete(id);
|
|
93
|
-
}
|
|
94
|
-
} catch (err) {
|
|
95
|
-
if (!(err instanceof NotFoundError)) {
|
|
96
|
-
throw err;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
|
|
102
|
-
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
103
|
-
const idxName = indexName(cls, idx);
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
105
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
106
|
-
let index = this.#indices[idx.type].get(idxName)?.get(key);
|
|
107
|
-
|
|
108
|
-
if (!index) {
|
|
109
|
-
if (!this.#indices[idx.type].has(idxName)) {
|
|
110
|
-
this.#indices[idx.type].set(idxName, new Map());
|
|
111
|
-
}
|
|
112
|
-
if (idx.type === 'sorted') {
|
|
113
|
-
this.#indices[idx.type].get(idxName)!.set(key, index = new Map());
|
|
114
|
-
} else {
|
|
115
|
-
this.#indices[idx.type].get(idxName)!.set(key, index = new Set());
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (index instanceof Map) {
|
|
120
|
-
index?.set(item.id, +sort!);
|
|
121
|
-
} else {
|
|
122
|
-
index?.add(item.id);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'remove'): Promise<void>;
|
|
128
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'write'): Promise<T>;
|
|
129
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'write' | 'remove'): Promise<T | void> {
|
|
130
|
-
const store = this.#getStore(cls);
|
|
131
|
-
await this.#removeIndices(cls, item.id);
|
|
132
|
-
if (action === 'write') {
|
|
133
|
-
store.set(item.id, Buffer.from(JSON.stringify(item)));
|
|
134
|
-
await this.#writeIndices(cls, item);
|
|
135
|
-
return item;
|
|
136
|
-
} else {
|
|
137
|
-
store.delete(item.id);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
|
|
142
|
-
const config = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
143
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, config, body);
|
|
144
|
-
const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
|
|
145
|
-
let id: string | undefined;
|
|
146
|
-
if (index) {
|
|
147
|
-
if (index instanceof Map) {
|
|
148
|
-
id = getFirstId(index, +sort!); // Grab first id
|
|
149
|
-
} else {
|
|
150
|
-
id = getFirstId(index); // Grab first id
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (id) {
|
|
154
|
-
return id;
|
|
155
|
-
}
|
|
156
|
-
throw new NotFoundError(cls, key);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async postConstruct(): Promise<void> {
|
|
160
|
-
await ModelStorageUtil.registerModelChangeListener(this);
|
|
161
|
-
ModelExpiryUtil.registerCull(this);
|
|
162
|
-
|
|
163
|
-
for (const el of ModelRegistry.getClasses()) {
|
|
164
|
-
for (const idx of ModelRegistry.get(el).indices ?? []) {
|
|
165
|
-
switch (idx.type) {
|
|
166
|
-
case 'unique': {
|
|
167
|
-
console.error('Unique indices are not supported for', { cls: el.Ⲑid, idx: idx.name });
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// CRUD Support
|
|
176
|
-
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
177
|
-
const store = this.#getStore(cls);
|
|
178
|
-
if (store.has(id)) {
|
|
179
|
-
const res = await ModelCrudUtil.load(cls, store.get(id)!);
|
|
180
|
-
if (res) {
|
|
181
|
-
if (ModelRegistry.get(cls).expiresAt) {
|
|
182
|
-
if (!ModelExpiryUtil.getExpiryState(cls, res).expired) {
|
|
183
|
-
return res;
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
return res;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
throw new NotFoundError(cls, id);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
194
|
-
if (!item.id) {
|
|
195
|
-
item.id = this.idSource.create();
|
|
196
|
-
}
|
|
197
|
-
this.#find(cls, item.id, 'data');
|
|
198
|
-
return await this.upsert(cls, item);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
202
|
-
await this.get(cls, item.id);
|
|
203
|
-
return await this.upsert(cls, item);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
207
|
-
const store = this.#getStore(cls);
|
|
208
|
-
if (item.id && store.has(item.id)) {
|
|
209
|
-
await ModelCrudUtil.load(cls, store.get(item.id)!, 'exists');
|
|
210
|
-
}
|
|
211
|
-
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
212
|
-
return await this.#persist(cls, prepped, 'write');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
216
|
-
const id = item.id;
|
|
217
|
-
const clean = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
218
|
-
return await this.#persist(cls, clean, 'write');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
222
|
-
const store = this.#getStore(cls);
|
|
223
|
-
if (!store.has(id)) {
|
|
224
|
-
throw new NotFoundError(cls, id);
|
|
225
|
-
}
|
|
226
|
-
await ModelCrudUtil.load(cls, store.get(id)!);
|
|
227
|
-
const where: ModelType = { id };
|
|
228
|
-
await this.#persist(cls, where, 'remove');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
232
|
-
for (const id of this.#getStore(cls).keys()) {
|
|
233
|
-
try {
|
|
234
|
-
yield await this.get(cls, id);
|
|
235
|
-
} catch (err) {
|
|
236
|
-
if (!(err instanceof NotFoundError)) {
|
|
237
|
-
throw err;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Stream Support
|
|
244
|
-
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
245
|
-
const streams = this.#getStore(STREAMS);
|
|
246
|
-
const metaContent = this.#getStore(STREAM_META);
|
|
247
|
-
metaContent.set(location, Buffer.from(JSON.stringify(meta)));
|
|
248
|
-
streams.set(location, await toBuffer(input));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async getStream(location: string, range?: StreamRange): Promise<Readable> {
|
|
252
|
-
const streams = this.#find(STREAMS, location, 'notfound');
|
|
253
|
-
let buffer = streams.get(location)!;
|
|
254
|
-
if (range) {
|
|
255
|
-
range = enforceRange(range, buffer.length);
|
|
256
|
-
buffer = buffer.subarray(range.start, range.end! + 1);
|
|
257
|
-
}
|
|
258
|
-
return Readable.from(buffer);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async describeStream(location: string): Promise<StreamMeta> {
|
|
262
|
-
const metaContent = this.#find(STREAM_META, location, 'notfound');
|
|
263
|
-
const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
|
|
264
|
-
return meta;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async deleteStream(location: string): Promise<void> {
|
|
268
|
-
const streams = this.#getStore(STREAMS);
|
|
269
|
-
const metaContent = this.#getStore(STREAM_META);
|
|
270
|
-
if (streams.has(location)) {
|
|
271
|
-
streams.delete(location);
|
|
272
|
-
metaContent.delete(location);
|
|
273
|
-
} else {
|
|
274
|
-
throw new NotFoundError('Stream', location);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Expiry
|
|
279
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
280
|
-
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Storage Support
|
|
284
|
-
async createStorage(): Promise<void> {
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async deleteStorage(): Promise<void> {
|
|
288
|
-
this.#store.clear();
|
|
289
|
-
this.#indices.sorted.clear();
|
|
290
|
-
this.#indices.unsorted.clear();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
async createModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
295
|
-
for (const idx of ModelRegistry.get(cls).indices ?? []) {
|
|
296
|
-
if (idx.type === 'sorted' || idx.type === 'unsorted') {
|
|
297
|
-
this.#indices[idx.type].set(indexName(cls, idx), new Map());
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
303
|
-
if (cls === StreamModel) {
|
|
304
|
-
this.#getStore(STREAMS).clear();
|
|
305
|
-
this.#getStore(STREAM_META).clear();
|
|
306
|
-
} else {
|
|
307
|
-
this.#getStore(cls).clear();
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Indexed
|
|
312
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
|
|
313
|
-
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
|
|
317
|
-
await this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
|
|
321
|
-
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
325
|
-
const config = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
326
|
-
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body, { emptySortValue: null });
|
|
327
|
-
const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
|
|
328
|
-
|
|
329
|
-
if (index) {
|
|
330
|
-
if (index instanceof Set) {
|
|
331
|
-
for (const id of index) {
|
|
332
|
-
yield this.get(cls, id);
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
for (const id of [...index.entries()].sort((a, b) => +a[1] - +b[1]).map(([a, b]) => a)) {
|
|
336
|
-
yield this.get(cls, id);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
package/src/service/stream.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
|
|
3
|
-
export interface StreamMeta {
|
|
4
|
-
/**
|
|
5
|
-
* File size
|
|
6
|
-
*/
|
|
7
|
-
size: number;
|
|
8
|
-
/**
|
|
9
|
-
* Mime type of the content
|
|
10
|
-
*/
|
|
11
|
-
contentType: string;
|
|
12
|
-
/**
|
|
13
|
-
* Hash of the file contents. Different files with the same name, will have the same hash
|
|
14
|
-
*/
|
|
15
|
-
hash: string;
|
|
16
|
-
/**
|
|
17
|
-
* The original base filename of the file
|
|
18
|
-
*/
|
|
19
|
-
filename: string;
|
|
20
|
-
/**
|
|
21
|
-
* Filenames title, optional for elements like images, audio, videos
|
|
22
|
-
*/
|
|
23
|
-
title?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Content encoding
|
|
26
|
-
*/
|
|
27
|
-
contentEncoding?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Content language
|
|
30
|
-
*/
|
|
31
|
-
contentLanguage?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Cache control
|
|
34
|
-
*/
|
|
35
|
-
cacheControl?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type StreamRange = { start: number, end?: number };
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Support for Streams CRD. Stream update is not supported.
|
|
42
|
-
*
|
|
43
|
-
* @concrete ../internal/service/common#ModelStreamSupportTarget
|
|
44
|
-
*/
|
|
45
|
-
export interface ModelStreamSupport {
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Upsert stream to storage
|
|
49
|
-
* @param location The location of the stream
|
|
50
|
-
* @param input The actual stream to write
|
|
51
|
-
* @param meta The stream metadata
|
|
52
|
-
*/
|
|
53
|
-
upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get stream from asset store
|
|
57
|
-
* @param location The location of the stream
|
|
58
|
-
*/
|
|
59
|
-
getStream(location: string, range?: StreamRange): Promise<Readable>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get metadata for stream
|
|
63
|
-
* @param location The location of the stream
|
|
64
|
-
*/
|
|
65
|
-
describeStream(location: string): Promise<StreamMeta>;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Delete stream by location
|
|
69
|
-
* @param location The location of the stream
|
|
70
|
-
*/
|
|
71
|
-
deleteStream(location: string): Promise<void>;
|
|
72
|
-
}
|