@travetto/model-file 8.0.0-alpha.9 → 8.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 +2 -2
- package/__index__.ts +1 -1
- package/package.json +18 -18
- package/src/service.ts +80 -41
package/README.md
CHANGED
|
@@ -14,6 +14,6 @@ yarn add @travetto/model-file
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
This module provides an file-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). Supported features:
|
|
17
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
18
|
-
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
19
17
|
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
|
|
18
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
|
|
19
|
+
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
package/__index__.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './src/service.ts';
|
|
1
|
+
export * from './src/service.ts';
|
package/package.json
CHANGED
|
@@ -1,39 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-file",
|
|
3
|
-
"version": "8.0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "8.0.1",
|
|
5
4
|
"description": "File system backing for the travetto model module.",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"datastore",
|
|
8
7
|
"file",
|
|
9
8
|
"schema",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"travetto",
|
|
10
|
+
"typescript"
|
|
12
11
|
],
|
|
13
12
|
"homepage": "https://travetto.io",
|
|
14
13
|
"license": "MIT",
|
|
15
14
|
"author": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"name": "Travetto Framework",
|
|
16
|
+
"email": "travetto.framework@gmail.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "git+https://github.com/travetto/travetto.git",
|
|
20
|
+
"directory": "module/model-file"
|
|
18
21
|
},
|
|
19
22
|
"files": [
|
|
20
23
|
"__index__.ts",
|
|
21
24
|
"src"
|
|
22
25
|
],
|
|
26
|
+
"type": "module",
|
|
23
27
|
"main": "__index__.ts",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"directory": "module/model-file"
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^8.0.
|
|
30
|
-
"@travetto/di": "^8.0.
|
|
31
|
-
"@travetto/model": "^8.0.
|
|
32
|
-
"@travetto/schema": "^8.0.
|
|
32
|
+
"@travetto/config": "^8.0.1",
|
|
33
|
+
"@travetto/di": "^8.0.1",
|
|
34
|
+
"@travetto/model": "^8.0.1",
|
|
35
|
+
"@travetto/schema": "^8.0.1"
|
|
33
36
|
},
|
|
34
37
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^8.0.
|
|
36
|
-
"@travetto/test": "^8.0.
|
|
38
|
+
"@travetto/cli": "^8.0.1",
|
|
39
|
+
"@travetto/test": "^8.0.1"
|
|
37
40
|
},
|
|
38
41
|
"peerDependenciesMeta": {
|
|
39
42
|
"@travetto/cli": {
|
|
@@ -45,8 +48,5 @@
|
|
|
45
48
|
},
|
|
46
49
|
"travetto": {
|
|
47
50
|
"displayName": "File Model Support"
|
|
48
|
-
},
|
|
49
|
-
"publishConfig": {
|
|
50
|
-
"access": "public"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
type Class, type TimeSpan, Runtime, type BinaryMetadata, type ByteRange, type BinaryType,
|
|
8
|
-
BinaryUtil, JSONUtil, BinaryMetadataUtil
|
|
9
|
-
} from '@travetto/runtime';
|
|
10
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
11
6
|
import { Config } from '@travetto/config';
|
|
12
|
-
import {
|
|
7
|
+
import { Injectable, PostConstruct } from '@travetto/di';
|
|
13
8
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
ExistsError,
|
|
10
|
+
type ModelBlobSupport,
|
|
11
|
+
type ModelCrudSupport,
|
|
12
|
+
ModelCrudUtil,
|
|
13
|
+
type ModelExpirySupport,
|
|
14
|
+
ModelExpiryUtil,
|
|
15
|
+
type ModelListOptions,
|
|
16
|
+
ModelRegistryIndex,
|
|
17
|
+
type ModelStorageSupport,
|
|
18
|
+
type ModelType,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
type OptionalId
|
|
16
21
|
} from '@travetto/model';
|
|
22
|
+
import {
|
|
23
|
+
type BinaryMetadata,
|
|
24
|
+
BinaryMetadataUtil,
|
|
25
|
+
type BinaryType,
|
|
26
|
+
BinaryUtil,
|
|
27
|
+
type ByteRange,
|
|
28
|
+
type Class,
|
|
29
|
+
JSONUtil,
|
|
30
|
+
Runtime,
|
|
31
|
+
type TimeSpan
|
|
32
|
+
} from '@travetto/runtime';
|
|
33
|
+
import { Required } from '@travetto/schema';
|
|
17
34
|
|
|
18
35
|
type Suffix = '.bin' | '.meta' | '.json' | '.expires';
|
|
19
36
|
|
|
@@ -32,33 +49,58 @@ export class FileModelConfig {
|
|
|
32
49
|
|
|
33
50
|
@PostConstruct()
|
|
34
51
|
async finalizeConfig(): Promise<void> {
|
|
35
|
-
this.folder ??= path.resolve(os.tmpdir(), `trv_file_${Runtime.main.name.replace(/[^a-z]/
|
|
52
|
+
this.folder ??= path.resolve(os.tmpdir(), `trv_file_${Runtime.main.name.replace(/[^a-z]/gi, '_')}`);
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
const exists = (file: string): Promise<boolean> =>
|
|
56
|
+
const exists = (file: string): Promise<boolean> =>
|
|
57
|
+
fs.stat(file).then(
|
|
58
|
+
() => true,
|
|
59
|
+
() => false
|
|
60
|
+
);
|
|
40
61
|
|
|
41
62
|
/**
|
|
42
63
|
* Standard file support
|
|
43
64
|
*/
|
|
44
65
|
@Injectable()
|
|
45
66
|
export class FileModelService implements ModelCrudSupport, ModelBlobSupport, ModelExpirySupport, ModelStorageSupport {
|
|
46
|
-
|
|
47
67
|
/** @private */
|
|
48
|
-
static async *
|
|
68
|
+
static async *scanFolder(folder: string, suffix: string, options?: ModelListOptions): AsyncGenerator<[id: string, field: string][]> {
|
|
69
|
+
const found: [id: string, field: string][] = [];
|
|
70
|
+
const batchSize = options?.batchSizeHint ?? 100;
|
|
71
|
+
const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
72
|
+
let produced = 0;
|
|
49
73
|
for (const sub of await fs.readdir(folder)) {
|
|
74
|
+
if (options?.abort?.aborted) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
50
77
|
for (const file of await fs.readdir(path.resolve(folder, sub))) {
|
|
78
|
+
if (options?.abort?.aborted) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
51
81
|
if (file.endsWith(suffix)) {
|
|
52
|
-
|
|
82
|
+
found.push([file.replace(suffix, ''), path.resolve(folder, sub, file)]);
|
|
83
|
+
produced += 1;
|
|
84
|
+
if (produced >= maxCount) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (found.length >= batchSize) {
|
|
88
|
+
yield found.splice(0, found.length);
|
|
89
|
+
}
|
|
53
90
|
}
|
|
54
91
|
}
|
|
55
92
|
}
|
|
93
|
+
if (found.length) {
|
|
94
|
+
yield found;
|
|
95
|
+
}
|
|
56
96
|
}
|
|
57
97
|
|
|
58
98
|
idSource = ModelCrudUtil.uuidSource();
|
|
59
99
|
config: FileModelConfig;
|
|
60
100
|
|
|
61
|
-
constructor(config: FileModelConfig) {
|
|
101
|
+
constructor(config: FileModelConfig) {
|
|
102
|
+
this.config = config;
|
|
103
|
+
}
|
|
62
104
|
|
|
63
105
|
/**
|
|
64
106
|
* The root location for all activity
|
|
@@ -160,30 +202,28 @@ export class FileModelService implements ModelCrudSupport, ModelBlobSupport, Mod
|
|
|
160
202
|
await fs.unlink(file);
|
|
161
203
|
}
|
|
162
204
|
|
|
163
|
-
async *
|
|
164
|
-
for await (const
|
|
165
|
-
|
|
166
|
-
yield await this.get(cls, id);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
if (!(error instanceof NotFoundError)) {
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
205
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
206
|
+
for await (const batch of FileModelService.scanFolder(await this.#resolveName(cls, '.json'), '.json', options)) {
|
|
207
|
+
yield ModelCrudUtil.filterOutNotFound(batch.map(([id]) => this.get(cls, id)));
|
|
172
208
|
}
|
|
173
209
|
}
|
|
174
210
|
|
|
175
211
|
// Blob
|
|
176
212
|
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
177
|
-
if (
|
|
213
|
+
if (
|
|
214
|
+
!overwrite &&
|
|
215
|
+
(await this.getBlobMetadata(location).then(
|
|
216
|
+
() => true,
|
|
217
|
+
() => false
|
|
218
|
+
))
|
|
219
|
+
) {
|
|
178
220
|
return;
|
|
179
221
|
}
|
|
180
222
|
const resolved = await BinaryMetadataUtil.compute(input, metadata);
|
|
181
223
|
const file = await this.#resolveName(ModelBlobNamespace, BIN, location);
|
|
182
224
|
await Promise.all([
|
|
183
225
|
BinaryUtil.pipeline(input, createWriteStream(file)),
|
|
184
|
-
BinaryUtil.pipeline(
|
|
185
|
-
JSONUtil.toBinaryArray(resolved),
|
|
186
|
-
createWriteStream(file.replace(BIN, META)))
|
|
226
|
+
BinaryUtil.pipeline(JSONUtil.toBinaryArray(resolved), createWriteStream(file.replace(BIN, META)))
|
|
187
227
|
]);
|
|
188
228
|
}
|
|
189
229
|
|
|
@@ -204,10 +244,7 @@ export class FileModelService implements ModelCrudSupport, ModelBlobSupport, Mod
|
|
|
204
244
|
async deleteBlob(location: string): Promise<void> {
|
|
205
245
|
const file = await this.#resolveName(ModelBlobNamespace, BIN, location);
|
|
206
246
|
if (await exists(file)) {
|
|
207
|
-
await Promise.all([
|
|
208
|
-
fs.unlink(file),
|
|
209
|
-
fs.unlink(file.replace('.bin', META))
|
|
210
|
-
]);
|
|
247
|
+
await Promise.all([fs.unlink(file), fs.unlink(file.replace('.bin', META))]);
|
|
211
248
|
} else {
|
|
212
249
|
throw new NotFoundError(ModelBlobNamespace, location);
|
|
213
250
|
}
|
|
@@ -221,14 +258,16 @@ export class FileModelService implements ModelCrudSupport, ModelBlobSupport, Mod
|
|
|
221
258
|
// Expiry
|
|
222
259
|
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
223
260
|
let deleted = 0;
|
|
224
|
-
for await (const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
261
|
+
for await (const batch of FileModelService.scanFolder(await this.#resolveName(cls, '.json'), '.json')) {
|
|
262
|
+
for (const [_id, file] of batch) {
|
|
263
|
+
try {
|
|
264
|
+
const item = await ModelCrudUtil.load(cls, await fs.readFile(file));
|
|
265
|
+
if (ModelExpiryUtil.getExpiryState(cls, item).expired) {
|
|
266
|
+
await fs.rm(file, { force: true });
|
|
267
|
+
deleted += 1;
|
|
268
|
+
}
|
|
269
|
+
} catch {} // Don't let a single failure stop the process
|
|
270
|
+
}
|
|
232
271
|
}
|
|
233
272
|
return deleted;
|
|
234
273
|
}
|
|
@@ -250,4 +289,4 @@ export class FileModelService implements ModelCrudSupport, ModelBlobSupport, Mod
|
|
|
250
289
|
async truncateBlob(): Promise<void> {
|
|
251
290
|
await fs.rm(await this.#resolveName(ModelBlobNamespace), { recursive: true, force: true });
|
|
252
291
|
}
|
|
253
|
-
}
|
|
292
|
+
}
|