@travetto/model 5.0.0-rc.9 → 5.0.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 +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 +14 -36
- package/src/internal/service/crud.ts +22 -45
- package/src/internal/service/indexed.ts +0 -1
- package/src/service/blob.ts +36 -0
- 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/blob.ts +136 -0
- package/support/test/crud.ts +8 -8
- package/support/test/polymorphism.ts +3 -0
- package/support/test/suite.ts +5 -5
- package/src/internal/service/stream.ts +0 -22
- package/src/provider/file.ts +0 -231
- package/src/provider/memory.ts +0 -339
- package/src/service/stream.ts +0 -72
- package/support/test/stream.ts +0 -113
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
4
|
+
import { BaseModelSuite } from '@travetto/model/support/test/base';
|
|
5
|
+
import { BinaryUtil, Util } from '@travetto/runtime';
|
|
6
|
+
|
|
7
|
+
import { ModelBlobSupport } from '../../src/service/blob';
|
|
8
|
+
import { ModelBlobUtil } from '../../src/internal/service/blob';
|
|
9
|
+
|
|
10
|
+
const meta = BinaryUtil.getBlobMeta;
|
|
11
|
+
|
|
12
|
+
@Suite()
|
|
13
|
+
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
14
|
+
|
|
15
|
+
fixture = new TestFixtures(['@travetto/model']);
|
|
16
|
+
|
|
17
|
+
@Test()
|
|
18
|
+
async writeBasic(): Promise<void> {
|
|
19
|
+
const service = await this.service;
|
|
20
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
21
|
+
|
|
22
|
+
const id = Util.uuid();
|
|
23
|
+
|
|
24
|
+
await service.upsertBlob(id, buffer);
|
|
25
|
+
const m = await service.describeBlob(id);
|
|
26
|
+
const retrieved = await service.describeBlob(id);
|
|
27
|
+
assert.deepStrictEqual(m, retrieved);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test()
|
|
31
|
+
async upsert(): Promise<void> {
|
|
32
|
+
const service = await this.service;
|
|
33
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
34
|
+
|
|
35
|
+
const id = Util.uuid();
|
|
36
|
+
|
|
37
|
+
await service.upsertBlob(id, buffer, { hash: '10' });
|
|
38
|
+
assert((await service.describeBlob(id)).hash === '10');
|
|
39
|
+
|
|
40
|
+
await service.upsertBlob(id, buffer, { hash: '20' });
|
|
41
|
+
assert((await service.describeBlob(id)).hash === '20');
|
|
42
|
+
|
|
43
|
+
await service.upsertBlob(id, buffer, { hash: '30' }, false);
|
|
44
|
+
assert((await service.describeBlob(id)).hash === '20');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test()
|
|
48
|
+
async writeStream(): Promise<void> {
|
|
49
|
+
const service = await this.service;
|
|
50
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
51
|
+
|
|
52
|
+
const id = Util.uuid();
|
|
53
|
+
await service.upsertBlob(id, buffer);
|
|
54
|
+
const { hash } = await service.describeBlob(id);
|
|
55
|
+
|
|
56
|
+
const retrieved = await service.getBlob(id);
|
|
57
|
+
const { hash: received } = meta(retrieved)!;
|
|
58
|
+
assert(hash === received);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test()
|
|
62
|
+
async writeAndDelete(): Promise<void> {
|
|
63
|
+
const service = await this.service;
|
|
64
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
65
|
+
|
|
66
|
+
const id = Util.uuid();
|
|
67
|
+
await service.upsertBlob(id, buffer);
|
|
68
|
+
|
|
69
|
+
await service.deleteBlob(id);
|
|
70
|
+
|
|
71
|
+
await assert.rejects(async () => {
|
|
72
|
+
await service.getBlob(id);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test()
|
|
77
|
+
async partialStream(): Promise<void> {
|
|
78
|
+
const service = await this.service;
|
|
79
|
+
const buffer = await this.fixture.read('/text.txt', true);
|
|
80
|
+
|
|
81
|
+
const id = Util.uuid();
|
|
82
|
+
await service.upsertBlob(id, buffer);
|
|
83
|
+
|
|
84
|
+
const retrieved = await service.getBlob(id);
|
|
85
|
+
const content = await retrieved.text();
|
|
86
|
+
assert(content.startsWith('abc'));
|
|
87
|
+
assert(content.endsWith('xyz'));
|
|
88
|
+
|
|
89
|
+
const partial = await service.getBlob(id, { start: 10, end: 20 });
|
|
90
|
+
assert(partial.size === 11);
|
|
91
|
+
const partialMeta = meta(partial)!;
|
|
92
|
+
const subContent = await partial.text();
|
|
93
|
+
const range = await ModelBlobUtil.enforceRange({ start: 10, end: 20 }, partialMeta.size!);
|
|
94
|
+
assert(subContent.length === (range.end - range.start) + 1);
|
|
95
|
+
|
|
96
|
+
const og = await this.fixture.read('/text.txt');
|
|
97
|
+
|
|
98
|
+
assert(subContent === og.substring(10, 21));
|
|
99
|
+
|
|
100
|
+
const partialUnbounded = await service.getBlob(id, { start: 10 });
|
|
101
|
+
const partialUnboundedMeta = meta(partial)!;
|
|
102
|
+
const subContent2 = await partialUnbounded.text();
|
|
103
|
+
const range2 = await ModelBlobUtil.enforceRange({ start: 10 }, partialUnboundedMeta.size!);
|
|
104
|
+
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
105
|
+
assert(subContent2.startsWith('klm'));
|
|
106
|
+
assert(subContent2.endsWith('xyz'));
|
|
107
|
+
|
|
108
|
+
const partialSingle = await service.getBlob(id, { start: 10, end: 10 });
|
|
109
|
+
const subContent3 = await partialSingle.text();
|
|
110
|
+
assert(subContent3.length === 1);
|
|
111
|
+
assert(subContent3 === 'k');
|
|
112
|
+
|
|
113
|
+
const partialOverBounded = await service.getBlob(id, { start: 20, end: 40 });
|
|
114
|
+
const subContent4 = await partialOverBounded.text();
|
|
115
|
+
assert(subContent4.length === 6);
|
|
116
|
+
assert(subContent4.endsWith('xyz'));
|
|
117
|
+
|
|
118
|
+
await assert.rejects(() => service.getBlob(id, { start: -10, end: 10 }));
|
|
119
|
+
await assert.rejects(() => service.getBlob(id, { start: 30, end: 37 }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@Test()
|
|
124
|
+
async writeAndGet() {
|
|
125
|
+
const service = await this.service;
|
|
126
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
127
|
+
await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
|
|
128
|
+
const saved = await service.getBlob('orange');
|
|
129
|
+
const savedMeta = meta(saved)!;
|
|
130
|
+
|
|
131
|
+
assert('text/yaml' === savedMeta.contentType);
|
|
132
|
+
assert(buffer.length === savedMeta.size);
|
|
133
|
+
assert('asset.yml' === savedMeta.filename);
|
|
134
|
+
assert(undefined === savedMeta.hash);
|
|
135
|
+
}
|
|
136
|
+
}
|
package/support/test/crud.ts
CHANGED
|
@@ -125,22 +125,22 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
125
125
|
assert(o.id);
|
|
126
126
|
assert(o.name === 'bob');
|
|
127
127
|
|
|
128
|
-
const o2 = await service.updatePartial(Person,
|
|
128
|
+
const o2 = await service.updatePartial(Person, {
|
|
129
129
|
id: o.id,
|
|
130
130
|
name: 'oscar'
|
|
131
|
-
})
|
|
131
|
+
});
|
|
132
132
|
|
|
133
133
|
assert(o2.name === 'oscar');
|
|
134
134
|
assert(o2.age === 20);
|
|
135
135
|
assert(o2.address.street2 === 'roader');
|
|
136
136
|
|
|
137
|
-
await service.updatePartial(Person,
|
|
137
|
+
await service.updatePartial(Person, {
|
|
138
138
|
id: o2.id,
|
|
139
139
|
gender: 'f',
|
|
140
140
|
address: {
|
|
141
141
|
street1: 'changed\n',
|
|
142
142
|
}
|
|
143
|
-
})
|
|
143
|
+
});
|
|
144
144
|
|
|
145
145
|
const o3 = await service.get(Person, o.id);
|
|
146
146
|
|
|
@@ -175,11 +175,11 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
175
175
|
]
|
|
176
176
|
}));
|
|
177
177
|
|
|
178
|
-
const o2 = await service.updatePartial(SimpleList,
|
|
178
|
+
const o2 = await service.updatePartial(SimpleList, {
|
|
179
179
|
id: o.id,
|
|
180
180
|
names: ['a', 'd'],
|
|
181
181
|
simples: [{ name: 'd' }]
|
|
182
|
-
})
|
|
182
|
+
});
|
|
183
183
|
|
|
184
184
|
assert.deepStrictEqual(o2.names, ['a', 'd']);
|
|
185
185
|
assert.deepStrictEqual(o2.simples, [SimpleItem.from({ name: 'd' })]);
|
|
@@ -195,12 +195,12 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
195
195
|
assert(o.address === undefined);
|
|
196
196
|
assert(o.name === 'bob-suffix');
|
|
197
197
|
|
|
198
|
-
await service.updatePartial(User2,
|
|
198
|
+
await service.updatePartial(User2, {
|
|
199
199
|
id: o.id,
|
|
200
200
|
address: {
|
|
201
201
|
street1: 'blue'
|
|
202
202
|
}
|
|
203
|
-
})
|
|
203
|
+
});
|
|
204
204
|
|
|
205
205
|
const o3 = await service.get(User2, o.id);
|
|
206
206
|
|
|
@@ -133,6 +133,9 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
133
133
|
name: 'bob2'
|
|
134
134
|
}));
|
|
135
135
|
|
|
136
|
+
const all2 = await collect(service.list(Worker));
|
|
137
|
+
assert(all2.length === 4);
|
|
138
|
+
|
|
136
139
|
const engineers2 = await collect(service.list(Engineer));
|
|
137
140
|
assert(engineers2.length === 2);
|
|
138
141
|
}
|
package/support/test/suite.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { DependencyRegistry } from '@travetto/di';
|
|
|
3
3
|
import { RootRegistry } from '@travetto/registry';
|
|
4
4
|
import { SuiteRegistry, TestFixtures } from '@travetto/test';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { isBlobSupported, isStorageSupported } from '../../src/internal/service/common';
|
|
7
|
+
import { MODEL_BLOB } from '../../src/internal/service/blob';
|
|
8
8
|
import { ModelRegistry } from '../../src/registry/model';
|
|
9
9
|
|
|
10
10
|
const Loaded = Symbol();
|
|
@@ -61,11 +61,11 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
if (
|
|
64
|
+
if (isBlobSupported(service)) {
|
|
65
65
|
if (service.truncateModel) {
|
|
66
|
-
await service.truncateModel(
|
|
66
|
+
await service.truncateModel(MODEL_BLOB);
|
|
67
67
|
} else if (service.deleteModel) {
|
|
68
|
-
await service.deleteModel(
|
|
68
|
+
await service.deleteModel(MODEL_BLOB);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
} else {
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { AppError, Class } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import { ModelType } from '../../types/model';
|
|
4
|
-
import { StreamRange } from '../../service/stream';
|
|
5
|
-
|
|
6
|
-
class Cls { id: string; }
|
|
7
|
-
export const StreamModel: Class<ModelType> = Cls;
|
|
8
|
-
export const STREAMS = '_streams';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Enforce byte range for stream stream/file of a certain size
|
|
13
|
-
*/
|
|
14
|
-
export function enforceRange({ start, end }: StreamRange, size: number): Required<StreamRange> {
|
|
15
|
-
end = Math.min(end ?? size - 1, size - 1);
|
|
16
|
-
|
|
17
|
-
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
|
|
18
|
-
throw new AppError('Invalid position, out of range', 'data');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return { start, end };
|
|
22
|
-
}
|
package/src/provider/file.ts
DELETED
|
@@ -1,231 +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, asFull } 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
|
-
return asFull<T>(item);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
160
|
-
const file = await this.#find(cls, '.json', id);
|
|
161
|
-
await fs.unlink(file);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
165
|
-
for await (const [id] of FileModelService.scanFolder(await this.#resolveName(cls, '.json'), '.json')) {
|
|
166
|
-
try {
|
|
167
|
-
yield await this.get(cls, id);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
if (!(err instanceof NotFoundError)) {
|
|
170
|
-
throw err;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Stream
|
|
177
|
-
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
178
|
-
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
179
|
-
await Promise.all([
|
|
180
|
-
await pipeline(input, createWriteStream(file)),
|
|
181
|
-
fs.writeFile(file.replace(BIN, META), JSON.stringify(meta), 'utf8')
|
|
182
|
-
]);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async getStream(location: string, range?: StreamRange): Promise<Readable> {
|
|
186
|
-
const file = await this.#find(STREAMS, BIN, location);
|
|
187
|
-
if (range) {
|
|
188
|
-
const meta = await this.describeStream(location);
|
|
189
|
-
range = enforceRange(range, meta.size);
|
|
190
|
-
}
|
|
191
|
-
return createReadStream(file, range);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async describeStream(location: string): Promise<StreamMeta> {
|
|
195
|
-
const file = await this.#find(STREAMS, META, location);
|
|
196
|
-
const content = await fs.readFile(file);
|
|
197
|
-
const text: StreamMeta = JSON.parse(content.toString('utf8'));
|
|
198
|
-
return text;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async deleteStream(location: string): Promise<void> {
|
|
202
|
-
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
203
|
-
if (await exists(file)) {
|
|
204
|
-
await Promise.all([
|
|
205
|
-
fs.unlink(file),
|
|
206
|
-
fs.unlink(file.replace('.bin', META))
|
|
207
|
-
]);
|
|
208
|
-
} else {
|
|
209
|
-
throw new NotFoundError('Stream', location);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Expiry
|
|
214
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
215
|
-
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Storage management
|
|
219
|
-
async createStorage(): Promise<void> {
|
|
220
|
-
const dir = path.resolve(this.config.folder, this.config.namespace);
|
|
221
|
-
await fs.mkdir(dir, { recursive: true });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async deleteStorage(): Promise<void> {
|
|
225
|
-
await fs.rm(path.resolve(this.config.folder, this.config.namespace), { recursive: true, force: true });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async truncateModel(cls: Class<ModelType>): Promise<void> {
|
|
229
|
-
await fs.rm(await this.#resolveName(cls === StreamModel ? STREAMS : cls), { recursive: true, force: true });
|
|
230
|
-
}
|
|
231
|
-
}
|