@travetto/model 5.0.0-rc.1 → 5.0.0-rc.11

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.
Files changed (51) hide show
  1. package/README.md +85 -147
  2. package/__index__.ts +2 -4
  3. package/package.json +7 -7
  4. package/src/error/exists.ts +1 -1
  5. package/src/error/invalid-index.ts +1 -1
  6. package/src/error/invalid-sub-type.ts +1 -1
  7. package/src/error/not-found.ts +1 -1
  8. package/src/internal/service/blob.ts +5 -0
  9. package/src/internal/service/bulk.ts +1 -1
  10. package/src/internal/service/common.ts +20 -26
  11. package/src/internal/service/crud.ts +15 -21
  12. package/src/internal/service/expiry.ts +2 -5
  13. package/src/internal/service/indexed.ts +13 -26
  14. package/src/internal/service/storage.ts +3 -7
  15. package/src/registry/decorator.ts +7 -13
  16. package/src/registry/model.ts +13 -8
  17. package/src/registry/types.ts +1 -2
  18. package/src/service/basic.ts +1 -1
  19. package/src/service/blob.ts +41 -0
  20. package/src/service/bulk.ts +1 -1
  21. package/src/service/crud.ts +1 -1
  22. package/src/service/expiry.ts +1 -1
  23. package/src/service/indexed.ts +2 -2
  24. package/src/service/storage.ts +1 -1
  25. package/src/util/blob.ts +60 -0
  26. package/support/base-command.ts +1 -1
  27. package/support/bin/candidate.ts +2 -3
  28. package/support/bin/export.ts +1 -1
  29. package/support/bin/install.ts +1 -1
  30. package/support/cli.model_export.ts +1 -1
  31. package/support/cli.model_install.ts +1 -1
  32. package/support/doc.support.tsx +5 -11
  33. package/support/fixtures/alpha.txt +1 -0
  34. package/support/fixtures/empty +0 -0
  35. package/support/fixtures/empty.m4a +0 -0
  36. package/support/fixtures/logo.gif +0 -0
  37. package/support/fixtures/logo.png +0 -0
  38. package/support/fixtures/small-audio +0 -0
  39. package/support/fixtures/small-audio.mp3 +0 -0
  40. package/support/test/base.ts +3 -3
  41. package/support/test/blob.ts +117 -0
  42. package/support/test/crud.ts +5 -4
  43. package/support/test/expiry.ts +1 -1
  44. package/support/test/indexed.ts +1 -1
  45. package/support/test/polymorphism.ts +7 -6
  46. package/support/test/suite.ts +6 -6
  47. package/src/internal/service/stream.ts +0 -22
  48. package/src/provider/file.ts +0 -233
  49. package/src/provider/memory.ts +0 -341
  50. package/src/service/stream.ts +0 -72
  51. package/support/test/stream.ts +0 -110
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert';
2
+ import timers from 'node:timers/promises';
2
3
 
3
4
  import { Suite, Test } from '@travetto/test';
4
5
  import { Schema, Text, Precision, Required, } from '@travetto/schema';
@@ -48,7 +49,7 @@ class User2 {
48
49
  name: string;
49
50
 
50
51
  prePersist() {
51
- this.name = `${this.name}-suff`;
52
+ this.name = `${this.name}-suffix`;
52
53
  }
53
54
  }
54
55
 
@@ -192,7 +193,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
192
193
  }));
193
194
 
194
195
  assert(o.address === undefined);
195
- assert(o.name === 'bob-suff');
196
+ assert(o.name === 'bob-suffix');
196
197
 
197
198
  await service.updatePartial(User2, User2.from({
198
199
  id: o.id,
@@ -216,7 +217,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
216
217
  assert(res.createdDate instanceof Date);
217
218
  }
218
219
 
219
- @Test('verify prepersist on create/update')
220
+ @Test('verify pre-persist on create/update')
220
221
  async testPrePersist() {
221
222
  const service = await this.service;
222
223
  const res = await service.create(Dated, Dated.from({}));
@@ -224,7 +225,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
224
225
  assert(res.createdDate instanceof Date);
225
226
  assert(res.updatedDate instanceof Date);
226
227
 
227
- await new Promise(r => setTimeout(r, 100));
228
+ await timers.setTimeout(100);
228
229
 
229
230
  const final = await service.updatePartial(Dated, { id: res.id });
230
231
  assert(final.createdDate instanceof Date);
@@ -2,7 +2,7 @@ import assert from 'node:assert';
2
2
  import timers from 'node:timers/promises';
3
3
 
4
4
  import { Suite, Test } from '@travetto/test';
5
- import { TimeSpan, TimeUnit, TimeUtil } from '@travetto/base';
5
+ import { TimeSpan, TimeUnit, TimeUtil } from '@travetto/runtime';
6
6
 
7
7
  import { ExpiresAt, Model } from '../../src/registry/decorator';
8
8
  import { ModelExpirySupport } from '../../src/service/expiry';
@@ -2,7 +2,7 @@ import assert from 'node:assert';
2
2
 
3
3
  import { Suite, Test } from '@travetto/test';
4
4
  import { Schema } from '@travetto/schema';
5
- import { TimeUtil } from '@travetto/base';
5
+ import { TimeUtil } from '@travetto/runtime';
6
6
 
7
7
  import { Index, Model } from '../../src/registry/decorator';
8
8
  import { ModelIndexedSupport } from '../../src/service/indexed';
@@ -12,6 +12,7 @@ import { isIndexedSupported } from '../../src/internal/service/common';
12
12
  import { ExistsError } from '../../src/error/exists';
13
13
 
14
14
  import { BaseModelSuite } from './base';
15
+ import { castTo } from '@travetto/runtime';
15
16
 
16
17
  @Model({ baseType: true })
17
18
  export class Worker {
@@ -70,7 +71,7 @@ export class IndexedEngineer extends IndexedWorker {
70
71
  }
71
72
 
72
73
  async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
73
- const out = [] as T[];
74
+ const out: T[] = [];
74
75
  for await (const el of iterable) {
75
76
  out.push(el);
76
77
  }
@@ -116,12 +117,12 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
116
117
 
117
118
  const fire3 = all.find(x => x instanceof Firefighter);
118
119
  assert(fire3 instanceof Firefighter);
119
- assert((fire3 as Firefighter).firehouse === 20);
120
+ assert(fire3.firehouse === 20);
120
121
  assert(fire3.name === 'rob');
121
122
 
122
123
  const eng3 = all.find(x => x instanceof Engineer);
123
124
  assert(eng3 instanceof Engineer);
124
- assert((eng3 as Engineer).major === 'oranges');
125
+ assert(eng3.major === 'oranges');
125
126
  assert(eng3.name === 'cob');
126
127
 
127
128
  const engineers = await collect(service.list(Engineer));
@@ -161,7 +162,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
161
162
  );
162
163
 
163
164
  await assert.rejects(
164
- () => service.update(Engineer, Doctor.from({ ...doc }) as unknown as Engineer),
165
+ () => service.update(Engineer, castTo(Doctor.from({ ...doc }))),
165
166
  (e: Error) => (e instanceof NotFoundError || e instanceof SubTypeNotSupportedError || e instanceof TypeMismatchError) ? undefined : e);
166
167
 
167
168
  await timers.setTimeout(15);
@@ -205,7 +206,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
205
206
 
206
207
  @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
207
208
  async polymorphicIndexGet() {
208
- const service = (await this.service) as unknown as ModelIndexedSupport;
209
+ const service: ModelIndexedSupport = castTo(await this.service);
209
210
  const now = 30;
210
211
  const [doc, fire, eng] = [
211
212
  IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
@@ -235,7 +236,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
235
236
 
236
237
  @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
237
238
  async polymorphicIndexDelete() {
238
- const service = (await this.service) as unknown as ModelIndexedSupport;
239
+ const service: ModelIndexedSupport = castTo(await this.service);
239
240
  const now = 30;
240
241
  const [doc, fire, eng] = [
241
242
  IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
@@ -1,10 +1,10 @@
1
- import { Class } from '@travetto/base';
1
+ import { Class } from '@travetto/runtime';
2
2
  import { DependencyRegistry } from '@travetto/di';
3
3
  import { RootRegistry } from '@travetto/registry';
4
4
  import { SuiteRegistry, TestFixtures } from '@travetto/test';
5
5
 
6
- import { isStorageSupported, isStreamSupported } from '../../src/internal/service/common';
7
- import { StreamModel } from '../../src/internal/service/stream';
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 (isStreamSupported(service)) {
64
+ if (isBlobSupported(service)) {
65
65
  if (service.truncateModel) {
66
- await service.truncateModel(StreamModel);
66
+ await service.truncateModel(MODEL_BLOB);
67
67
  } else if (service.deleteModel) {
68
- await service.deleteModel(StreamModel);
68
+ await service.deleteModel(MODEL_BLOB);
69
69
  }
70
70
  }
71
71
  } else {
@@ -1,22 +0,0 @@
1
- import { AppError, Class } from '@travetto/base';
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
- }
@@ -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, RuntimeContext } from '@travetto/base';
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_${RuntimeContext.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
- }