@travetto/model 7.1.4 → 8.0.0-alpha.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 CHANGED
@@ -182,10 +182,10 @@ export interface ModelBlobSupport {
182
182
  * Upsert blob to storage
183
183
  * @param location The location of the blob
184
184
  * @param input The actual blob to write
185
- * @param meta Additional metadata to store with the blob
185
+ * @param metadata Additional metadata to store with the blob
186
186
  * @param overwrite Should we replace content if already found, defaults to true
187
187
  */
188
- upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite?: boolean): Promise<void>;
188
+ upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite?: boolean): Promise<void>;
189
189
 
190
190
  /**
191
191
  * Get blob from storage
@@ -197,7 +197,7 @@ export interface ModelBlobSupport {
197
197
  * Get metadata for blob
198
198
  * @param location The location of the blob
199
199
  */
200
- getBlobMeta(location: string): Promise<BlobMeta>;
200
+ getBlobMetadata(location: string): Promise<BinaryMetadata>;
201
201
 
202
202
  /**
203
203
  * Delete blob by location
@@ -208,25 +208,26 @@ export interface ModelBlobSupport {
208
208
  /**
209
209
  * Update blob metadata
210
210
  * @param location The location of the blob
211
+ * @param metadata The metadata to update
211
212
  */
212
- updateBlobMeta(location: string, meta: BlobMeta): Promise<void>;
213
+ updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
213
214
 
214
215
  /**
215
216
  * Produces an externally usable URL for sharing limited read access to a specific resource
216
217
  *
217
218
  * @param location The asset location to read from
218
- * @param exp Expiry
219
+ * @param expiresIn Expiry
219
220
  */
220
- getBlobReadUrl?(location: string, exp?: TimeSpan): Promise<string>;
221
+ getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
221
222
 
222
223
  /**
223
224
  * Produces an externally usable URL for sharing allowing direct write access
224
225
  *
225
226
  * @param location The asset location to write to
226
- * @param meta The metadata to associate with the final asset
227
- * @param exp Expiry
227
+ * @param metadata The metadata to associate with the final asset
228
+ * @param expiresIn Expiry
228
229
  */
229
- getBlobWriteUrl?(location: string, meta: BlobMeta, exp?: TimeSpan): Promise<string>;
230
+ getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
230
231
  }
231
232
  ```
232
233
 
@@ -278,7 +279,7 @@ To enforce that these contracts are honored, the module provides shared test sui
278
279
  **Code: Memory Service Test Configuration**
279
280
  ```typescript
280
281
  import { DependencyRegistryIndex } from '@travetto/di';
281
- import { AppError, castTo, type Class, classConstruct } from '@travetto/runtime';
282
+ import { RuntimeError, castTo, type Class, classConstruct } from '@travetto/runtime';
282
283
 
283
284
  import { ModelBulkUtil } from '../../src/util/bulk.ts';
284
285
  import { ModelCrudUtil } from '../../src/util/crud.ts';
@@ -306,7 +307,7 @@ export abstract class BaseModelSuite<T> {
306
307
  }
307
308
  return i;
308
309
  } else {
309
- throw new AppError(`Size is not supported for this service: ${this.serviceClass.name}`);
310
+ throw new RuntimeError(`Size is not supported for this service: ${this.serviceClass.name}`);
310
311
  }
311
312
  }
312
313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Datastore abstraction for core operations.",
6
6
  "keywords": [
@@ -27,14 +27,14 @@
27
27
  "directory": "module/model"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/config": "^7.1.4",
31
- "@travetto/di": "^7.1.4",
32
- "@travetto/registry": "^7.1.4",
33
- "@travetto/schema": "^7.1.4"
30
+ "@travetto/config": "^8.0.0-alpha.0",
31
+ "@travetto/di": "^8.0.0-alpha.0",
32
+ "@travetto/registry": "^8.0.0-alpha.0",
33
+ "@travetto/schema": "^8.0.0-alpha.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^7.1.4",
37
- "@travetto/test": "^7.1.4"
36
+ "@travetto/cli": "^8.0.0-alpha.0",
37
+ "@travetto/test": "^8.0.0-alpha.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@travetto/cli": {
@@ -1,9 +1,9 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Represents when a data item already exists
5
5
  */
6
- export class ExistsError extends AppError {
6
+ export class ExistsError extends RuntimeError {
7
7
  constructor(cls: Class | string, id: string) {
8
8
  super(`${typeof cls === 'string' ? cls : cls.name} with id ${id} already exists`, {
9
9
  category: 'data',
@@ -1,4 +1,4 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
 
3
3
  import type { IndexConfig } from '../registry/types.ts';
4
4
  import type { ModelType } from '../types/model.ts';
@@ -6,7 +6,7 @@ import type { ModelType } from '../types/model.ts';
6
6
  /**
7
7
  * Represents when an index is invalid
8
8
  */
9
- export class IndexNotSupported<T extends ModelType> extends AppError {
9
+ export class IndexNotSupported<T extends ModelType> extends RuntimeError {
10
10
  constructor(cls: Class<T>, idx: IndexConfig<T>, message: string = '') {
11
11
  super(`${typeof cls === 'string' ? cls : cls.name} and index ${idx.name} of type ${idx.type} is not supported. ${message}`.trim(), { category: 'data' });
12
12
  }
@@ -1,9 +1,9 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Represents when a model subtype class is unable to be used directly
5
5
  */
6
- export class SubTypeNotSupportedError extends AppError {
6
+ export class SubTypeNotSupportedError extends RuntimeError {
7
7
  constructor(cls: Class | string) {
8
8
  super(`${typeof cls === 'string' ? cls : cls.name} cannot be used for this operation`, { category: 'data' });
9
9
  }
@@ -1,9 +1,9 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Represents when a model of cls and id cannot be found
5
5
  */
6
- export class NotFoundError extends AppError {
6
+ export class NotFoundError extends RuntimeError {
7
7
  constructor(cls: Class | string, id: string, details: Record<string, unknown> = {}) {
8
8
  super(`${typeof cls === 'string' ? cls : cls.name} with id ${id} not found`, { category: 'notfound', details });
9
9
  }
@@ -1,4 +1,4 @@
1
- import { AppError, castTo, type Class, getClass } from '@travetto/runtime';
1
+ import { RuntimeError, castTo, type Class, getClass } from '@travetto/runtime';
2
2
  import { SchemaRegistryIndex } from '@travetto/schema';
3
3
 
4
4
  import type { ModelType } from '../types/model.ts';
@@ -30,7 +30,7 @@ export function Model(config: Partial<ModelConfig<ModelType>> | string = {}) {
30
30
  */
31
31
  export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
32
32
  if (indices.some(config => config.fields.some(field => field === 'id'))) {
33
- throw new AppError('Cannot create an index with the id field');
33
+ throw new RuntimeError('Cannot create an index with the id field');
34
34
  }
35
35
  return function (cls: Class<T>): void {
36
36
  ModelRegistryIndex.getForRegister(cls).register({ indices });
@@ -1,5 +1,5 @@
1
1
  import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
- import { AppError, castTo, type Class } from '@travetto/runtime';
2
+ import { RuntimeError, castTo, type Class } from '@travetto/runtime';
3
3
  import { SchemaRegistryIndex } from '@travetto/schema';
4
4
 
5
5
  import type { IndexConfig, IndexType, ModelConfig } from './types.ts';
@@ -77,7 +77,7 @@ export class ModelRegistryIndex implements RegistryIndex {
77
77
 
78
78
  // Don't allow two models with same class name, or same store name
79
79
  if (classes.size > 1) {
80
- throw new AppError('Duplicate models with same store name', {
80
+ throw new RuntimeError('Duplicate models with same store name', {
81
81
  details: { classes: [...classes].toSorted() }
82
82
  });
83
83
  }
@@ -122,7 +122,7 @@ export class ModelRegistryIndex implements RegistryIndex {
122
122
  getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
123
123
  const expiry = this.getConfig(cls).expiresAt;
124
124
  if (!expiry) {
125
- throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`);
125
+ throw new RuntimeError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`);
126
126
  }
127
127
  return castTo(expiry);
128
128
  }
@@ -1,7 +1,9 @@
1
- import type { Class, RetainPrimitiveFields } from '@travetto/runtime';
1
+ import type { Class, Primitive, ValidFields } from '@travetto/runtime';
2
2
 
3
3
  import type { ModelType } from '../types/model.ts';
4
4
 
5
+ type RetainPrimitiveFields<T> = Pick<T, ValidFields<T, Primitive | Date>>;
6
+
5
7
  export type SortClauseRaw<T> = {
6
8
  [P in keyof T]?:
7
9
  T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
package/src/types/blob.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BinaryInput, BlobMeta, ByteRange, TimeSpan } from '@travetto/runtime';
1
+ import type { BinaryType, BinaryMetadata, ByteRange, TimeSpan } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Support for Blobs CRUD.
@@ -11,10 +11,10 @@ export interface ModelBlobSupport {
11
11
  * Upsert blob to storage
12
12
  * @param location The location of the blob
13
13
  * @param input The actual blob to write
14
- * @param meta Additional metadata to store with the blob
14
+ * @param metadata Additional metadata to store with the blob
15
15
  * @param overwrite Should we replace content if already found, defaults to true
16
16
  */
17
- upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite?: boolean): Promise<void>;
17
+ upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite?: boolean): Promise<void>;
18
18
 
19
19
  /**
20
20
  * Get blob from storage
@@ -26,7 +26,7 @@ export interface ModelBlobSupport {
26
26
  * Get metadata for blob
27
27
  * @param location The location of the blob
28
28
  */
29
- getBlobMeta(location: string): Promise<BlobMeta>;
29
+ getBlobMetadata(location: string): Promise<BinaryMetadata>;
30
30
 
31
31
  /**
32
32
  * Delete blob by location
@@ -37,23 +37,24 @@ export interface ModelBlobSupport {
37
37
  /**
38
38
  * Update blob metadata
39
39
  * @param location The location of the blob
40
+ * @param metadata The metadata to update
40
41
  */
41
- updateBlobMeta(location: string, meta: BlobMeta): Promise<void>;
42
+ updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
42
43
 
43
44
  /**
44
45
  * Produces an externally usable URL for sharing limited read access to a specific resource
45
46
  *
46
47
  * @param location The asset location to read from
47
- * @param exp Expiry
48
+ * @param expiresIn Expiry
48
49
  */
49
- getBlobReadUrl?(location: string, exp?: TimeSpan): Promise<string>;
50
+ getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
50
51
 
51
52
  /**
52
53
  * Produces an externally usable URL for sharing allowing direct write access
53
54
  *
54
55
  * @param location The asset location to write to
55
- * @param meta The metadata to associate with the final asset
56
- * @param exp Expiry
56
+ * @param metadata The metadata to associate with the final asset
57
+ * @param expiresIn Expiry
57
58
  */
58
- getBlobWriteUrl?(location: string, meta: BlobMeta, exp?: TimeSpan): Promise<string>;
59
+ getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
59
60
  }
package/src/types/bulk.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
  import type { ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import type { ModelCrudSupport } from './crud.ts';
@@ -42,7 +42,7 @@ type BulkErrorItem = { message: string, type: string, errors?: ValidationError[]
42
42
  /**
43
43
  * Bulk processing error
44
44
  */
45
- export class BulkProcessError extends AppError<{ errors: BulkErrorItem[] }> {
45
+ export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }> {
46
46
  constructor(errors: { idx: number, error: ValidationResultError }[]) {
47
47
  super('Bulk processing errors have occurred', {
48
48
  category: 'data',
package/src/util/blob.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Readable } from 'node:stream';
2
- import { AppError, type BinaryInput, BinaryUtil, type BlobMeta, type ByteRange, hasFunction } from '@travetto/runtime';
1
+ import { hasFunction } from '@travetto/runtime';
3
2
  import type { ModelBlobSupport } from '../types/blob.ts';
4
3
 
5
4
  /**
@@ -11,39 +10,4 @@ export class ModelBlobUtil {
11
10
  * Type guard for determining if service supports blob operations
12
11
  */
13
12
  static isSupported = hasFunction<ModelBlobSupport>('getBlob');
14
-
15
- /**
16
- * Convert input to a Readable, and get what metadata is available
17
- */
18
- static async getInput(input: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
19
- let result: Readable;
20
- if (input instanceof Blob) {
21
- metadata = { ...BinaryUtil.getBlobMeta(input), ...metadata };
22
- metadata.size ??= input.size;
23
- result = Readable.fromWeb(input.stream());
24
- } else if (typeof input === 'object' && 'pipeThrough' in input) {
25
- result = Readable.fromWeb(input);
26
- } else if (typeof input === 'object' && 'pipe' in input) {
27
- result = input;
28
- } else {
29
- metadata.size = input.length;
30
- result = Readable.from(input);
31
- }
32
-
33
- return [result, metadata ?? {}];
34
- }
35
-
36
- /**
37
- * Enforce byte range for stream stream/file of a certain size
38
- */
39
- static enforceRange({ start, end }: ByteRange, size: number): Required<ByteRange> {
40
- // End is inclusive
41
- end = Math.min(end ?? (size - 1), size - 1);
42
-
43
- if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
44
- throw new AppError('Invalid position, out of range', { category: 'data' });
45
- }
46
-
47
- return { start, end };
48
- }
49
13
  }
package/src/util/crud.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { castTo, type Class, Util, AppError, hasFunction, JSONUtil } from '@travetto/runtime';
1
+ import { castTo, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
2
2
  import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import { ModelRegistryIndex } from '../registry/registry-index.ts';
@@ -9,6 +9,8 @@ import { SubTypeNotSupportedError } from '../error/invalid-sub-type.ts';
9
9
  import type { DataHandler, PrePersistScope } from '../registry/types.ts';
10
10
  import type { ModelCrudSupport } from '../types/crud.ts';
11
11
 
12
+ type ModelLoadInput = string | BinaryArray | object;
13
+
12
14
  export type ModelCrudProvider = {
13
15
  idSource: ModelIdSource;
14
16
  };
@@ -37,13 +39,11 @@ export class ModelCrudUtil {
37
39
  * @param cls Class to load model for
38
40
  * @param input Input as string or plain object
39
41
  */
40
- static async load<T extends ModelType>(cls: Class<T>, input: Buffer | string | object, onTypeMismatch: 'notfound' | 'exists' = 'notfound'): Promise<T> {
41
- let resolvedInput: object;
42
- if (typeof input === 'string' || input instanceof Buffer) {
43
- resolvedInput = JSONUtil.parseSafe(input);
44
- } else {
45
- resolvedInput = input;
46
- }
42
+ static async load<T extends ModelType>(cls: Class<T>, input: ModelLoadInput, onTypeMismatch: 'notfound' | 'exists' = 'notfound'): Promise<T> {
43
+ const resolvedInput: object =
44
+ typeof input === 'string' ? JSONUtil.fromUTF8(input) :
45
+ BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) :
46
+ input;
47
47
 
48
48
  const result = SchemaRegistryIndex.getBaseClass(cls).from(resolvedInput);
49
49
 
@@ -142,11 +142,11 @@ export class ModelCrudUtil {
142
142
  */
143
143
  static async prePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view?: string): Promise<Partial<T>> {
144
144
  if (!DataUtil.isPlainObject(item)) {
145
- throw new AppError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, { category: 'data' });
145
+ throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, { category: 'data' });
146
146
  }
147
147
  const keys = Object.keys(item);
148
148
  if ((keys.length === 1 && item.id) || keys.length === 0) {
149
- throw new AppError('No fields to update');
149
+ throw new RuntimeError('No fields to update');
150
150
  } else {
151
151
  item = { ...item };
152
152
  delete item.id;
@@ -34,7 +34,7 @@ export class ModelExpiryUtil {
34
34
  static registerCull(service: ModelExpirySupport & { readonly config?: { cullRate?: number | TimeSpan } }): void {
35
35
  const cullable = ModelRegistryIndex.getClasses().filter(cls => !!ModelRegistryIndex.getConfig(cls).expiresAt);
36
36
  if (service.deleteExpired && cullable.length) {
37
- const cullInterval = TimeUtil.asMillis(service.config?.cullRate ?? '10m');
37
+ const cullInterval = TimeUtil.duration(service.config?.cullRate ?? '10m', 'ms');
38
38
 
39
39
  (async (): Promise<void> => {
40
40
  await Util.nonBlockingTimeout(1000);
@@ -71,7 +71,6 @@ export class ModelIndexedUtil {
71
71
  if (empty === undefined || empty === Error) {
72
72
  throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
73
73
  }
74
- itemRef = castTo(empty!);
75
74
  } else {
76
75
  if (field !== sortField || (opts.includeSortInFields ?? true)) {
77
76
  fields.push({ path: parts, value: castTo(itemRef) });
@@ -22,7 +22,7 @@ export const Links = {
22
22
  Blob: toLink('Blob', toConcrete<ModelBlobSupport>()),
23
23
  };
24
24
 
25
- export const ModelTypes = (fn: | Function): DocJSXElement[] => {
25
+ export const ModelTypes = (fn: Function): DocJSXElement[] => {
26
26
  const { content } = DocFileUtil.readSource(fn);
27
27
  const found: DocJSXElementByFn<'CodeLink'>[] = [];
28
28
  const seen = new Set<string>();
@@ -1,5 +1,5 @@
1
1
  import { DependencyRegistryIndex } from '@travetto/di';
2
- import { AppError, castTo, type Class, classConstruct } from '@travetto/runtime';
2
+ import { RuntimeError, castTo, type Class, classConstruct } from '@travetto/runtime';
3
3
 
4
4
  import { ModelBulkUtil } from '../../src/util/bulk.ts';
5
5
  import { ModelCrudUtil } from '../../src/util/crud.ts';
@@ -27,7 +27,7 @@ export abstract class BaseModelSuite<T> {
27
27
  }
28
28
  return i;
29
29
  } else {
30
- throw new AppError(`Size is not supported for this service: ${this.serviceClass.name}`);
30
+ throw new RuntimeError(`Size is not supported for this service: ${this.serviceClass.name}`);
31
31
  }
32
32
  }
33
33
 
@@ -1,14 +1,11 @@
1
1
  import assert from 'node:assert';
2
2
 
3
3
  import { Suite, Test, TestFixtures } from '@travetto/test';
4
- import { BinaryUtil, Util } from '@travetto/runtime';
4
+ import { BinaryMetadataUtil, BinaryUtil, Util } from '@travetto/runtime';
5
5
 
6
6
  import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
7
7
 
8
8
  import type { ModelBlobSupport } from '../../src/types/blob.ts';
9
- import { ModelBlobUtil } from '../../src/util/blob.ts';
10
-
11
- const meta = BinaryUtil.getBlobMeta;
12
9
 
13
10
  @Suite()
14
11
  export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
@@ -18,51 +15,51 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
18
15
  @Test()
19
16
  async writeBasic(): Promise<void> {
20
17
  const service = await this.service;
21
- const buffer = await this.fixture.read('/asset.yml', true);
18
+ const buffer = await this.fixture.readBinaryArray('/asset.yml');
22
19
 
23
20
  const id = Util.uuid();
24
21
 
25
22
  await service.upsertBlob(id, buffer);
26
- const m = await service.getBlobMeta(id);
27
- const retrieved = await service.getBlobMeta(id);
23
+ const m = await service.getBlobMetadata(id);
24
+ const retrieved = await service.getBlobMetadata(id);
28
25
  assert.deepStrictEqual(m, retrieved);
29
26
  }
30
27
 
31
28
  @Test()
32
29
  async upsert(): Promise<void> {
33
30
  const service = await this.service;
34
- const buffer = await this.fixture.read('/asset.yml', true);
31
+ const buffer = await this.fixture.readBinaryArray('/asset.yml');
35
32
 
36
33
  const id = Util.uuid();
37
34
 
38
35
  await service.upsertBlob(id, buffer, { hash: '10' });
39
- assert((await service.getBlobMeta(id)).hash === '10');
36
+ assert((await service.getBlobMetadata(id)).hash === '10');
40
37
 
41
38
  await service.upsertBlob(id, buffer, { hash: '20' });
42
- assert((await service.getBlobMeta(id)).hash === '20');
39
+ assert((await service.getBlobMetadata(id)).hash === '20');
43
40
 
44
41
  await service.upsertBlob(id, buffer, { hash: '30' }, false);
45
- assert((await service.getBlobMeta(id)).hash === '20');
42
+ assert((await service.getBlobMetadata(id)).hash === '20');
46
43
  }
47
44
 
48
45
  @Test()
49
46
  async writeStream(): Promise<void> {
50
47
  const service = await this.service;
51
- const buffer = await this.fixture.read('/asset.yml', true);
48
+ const buffer = await this.fixture.readBinaryArray('/asset.yml');
52
49
 
53
50
  const id = Util.uuid();
54
51
  await service.upsertBlob(id, buffer);
55
- const { hash } = await service.getBlobMeta(id);
52
+ const { hash } = await service.getBlobMetadata(id);
56
53
 
57
54
  const retrieved = await service.getBlob(id);
58
- const { hash: received } = meta(retrieved)!;
55
+ const { hash: received } = BinaryMetadataUtil.read(retrieved)!;
59
56
  assert(hash === received);
60
57
  }
61
58
 
62
59
  @Test()
63
60
  async writeAndDelete(): Promise<void> {
64
61
  const service = await this.service;
65
- const buffer = await this.fixture.read('/asset.yml', true);
62
+ const buffer = await this.fixture.readBinaryArray('/asset.yml');
66
63
 
67
64
  const id = Util.uuid();
68
65
  await service.upsertBlob(id, buffer);
@@ -77,7 +74,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
77
74
  @Test()
78
75
  async partialStream(): Promise<void> {
79
76
  const service = await this.service;
80
- const buffer = await this.fixture.read('/text.txt', true);
77
+ const buffer = await this.fixture.readBinaryArray('/text.txt');
81
78
 
82
79
  const id = Util.uuid();
83
80
  await service.upsertBlob(id, buffer);
@@ -89,19 +86,19 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
89
86
 
90
87
  const partial = await service.getBlob(id, { start: 10, end: 20 });
91
88
  assert(partial.size === 11);
92
- const partialMeta = meta(partial)!;
89
+ const partialMeta = BinaryMetadataUtil.read(partial)!;
93
90
  const subContent = await partial.text();
94
- const range = await ModelBlobUtil.enforceRange({ start: 10, end: 20 }, partialMeta.size!);
91
+ const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
95
92
  assert(subContent.length === (range.end - range.start) + 1);
96
93
 
97
- const og = await this.fixture.read('/text.txt');
94
+ const og = await this.fixture.readText('/text.txt');
98
95
 
99
96
  assert(subContent === og.substring(10, 21));
100
97
 
101
98
  const partialUnbounded = await service.getBlob(id, { start: 10 });
102
- const partialUnboundedMeta = meta(partial)!;
99
+ const partialUnboundedMeta = BinaryMetadataUtil.read(partialUnbounded)!;
103
100
  const subContent2 = await partialUnbounded.text();
104
- const range2 = await ModelBlobUtil.enforceRange({ start: 10 }, partialUnboundedMeta.size!);
101
+ const range2 = BinaryMetadataUtil.enforceRange({ start: 10 }, partialUnboundedMeta);
105
102
  assert(subContent2.length === (range2.end - range2.start) + 1);
106
103
  assert(subContent2.startsWith('klm'));
107
104
  assert(subContent2.endsWith('xyz'));
@@ -123,15 +120,16 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
123
120
  @Test()
124
121
  async writeAndGet() {
125
122
  const service = await this.service;
126
- const buffer = await this.fixture.read('/asset.yml', true);
123
+ const buffer = await this.fixture.readBinaryArray('/asset.yml');
127
124
  await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
128
125
  const saved = await service.getBlob('orange');
129
- const savedMeta = meta(saved)!;
126
+ const savedMeta = BinaryMetadataUtil.read(saved)!;
127
+ console.error(savedMeta);
130
128
 
131
129
  assert('text/yaml' === savedMeta.contentType);
132
- assert(buffer.length === savedMeta.size);
130
+ assert(buffer.byteLength === savedMeta.size);
133
131
  assert('asset.yml' === savedMeta.filename);
134
- assert(undefined === savedMeta.hash);
132
+ assert(!!savedMeta.hash);
135
133
  }
136
134
 
137
135
  @Test()
@@ -140,16 +138,16 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
140
138
 
141
139
  await this.writeAndGet();
142
140
 
143
- await service.updateBlobMeta('orange', {
141
+ await service.updateBlobMetadata('orange', {
144
142
  contentType: 'text/yml',
145
143
  filename: 'orange.yml'
146
144
  });
147
145
 
148
- const savedMeta = await service.getBlobMeta('orange');
146
+ const savedMeta = await service.getBlobMetadata('orange');
149
147
 
150
148
  assert('text/yml' === savedMeta.contentType);
151
149
  assert('orange.yml' === savedMeta.filename);
152
- assert(undefined === savedMeta.hash);
150
+ assert(savedMeta.hash === undefined);
153
151
  }
154
152
 
155
153
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -157,38 +155,38 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
157
155
  async signedUrl() {
158
156
  const service = await this.service;
159
157
 
160
- const buffer = Buffer.alloc(1.5 * 10000);
161
- for (let i = 0; i < buffer.length; i++) {
162
- buffer.writeUInt8(Math.trunc(Math.random() * 255), i);
158
+ const bytes = BinaryUtil.binaryArrayToBuffer(BinaryUtil.makeBinaryArray(1.5 * 10000));
159
+ for (let i = 0; i < bytes.byteLength; i++) {
160
+ bytes.writeUInt8(Math.trunc(Math.random() * 255), i);
163
161
  }
164
162
 
165
163
  const writable = await service.getBlobWriteUrl!('largeFile/one', {
166
164
  contentType: 'image/jpeg',
167
165
  });
168
166
 
169
- console.log(writable);
170
167
  assert(writable);
171
168
 
172
169
  const response = await fetch(writable, {
173
170
  method: 'PUT',
174
- body: new File([buffer], 'gary', { type: 'image/jpeg' }),
171
+ body: new File([bytes], 'gary', { type: 'image/jpeg' }),
175
172
  });
176
173
 
177
174
  console.error(await response.text());
178
175
 
179
176
  assert(response.ok);
180
177
 
181
- await service.updateBlobMeta('largeFile/one', {
178
+ await service.updateBlobMetadata('largeFile/one', {
182
179
  contentType: 'image/jpeg',
183
180
  title: 'orange',
184
181
  filename: 'gary',
185
- size: buffer.length,
182
+ size: bytes.byteLength,
186
183
  });
187
184
 
188
185
  const found = await service.getBlob('largeFile/one');
189
- assert(found.size === buffer.length);
186
+ const foundMeta = BinaryMetadataUtil.read(found);
187
+ assert(found.size === bytes.byteLength);
190
188
  assert(found.type === 'image/jpeg');
191
- assert(BinaryUtil.getBlobMeta(found)?.title === 'orange');
192
- assert(BinaryUtil.getBlobMeta(found)?.filename === 'gary');
189
+ assert(foundMeta.title === 'orange');
190
+ assert(foundMeta.filename === 'gary');
193
191
  }
194
192
  }
@@ -67,6 +67,13 @@ class Dated {
67
67
  updatedDate: Date;
68
68
  }
69
69
 
70
+ @Model()
71
+ class BigIntModel {
72
+ id: string;
73
+ largeNumber: bigint;
74
+ optionalBigInt?: bigint;
75
+ }
76
+
70
77
  @Suite()
71
78
  export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
72
79
 
@@ -343,4 +350,39 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
343
350
  assert(o2.simples);
344
351
  assert(o2.simples.length === 1);
345
352
  }
353
+
354
+ @Test('Verify bigint storage and retrieval')
355
+ async testBigIntReadWrite() {
356
+ const service = await this.service;
357
+
358
+ // Create with bigint values
359
+ const created = await service.create(BigIntModel, BigIntModel.from({
360
+ largeNumber: 9007199254740991n, // Number.MAX_SAFE_INTEGER as bigint
361
+ optionalBigInt: 1234567890123456789n
362
+ }));
363
+
364
+ assert(created.id);
365
+ assert.strictEqual(created.largeNumber, 9007199254740991n);
366
+ assert.strictEqual(created.optionalBigInt, 1234567890123456789n);
367
+
368
+ // Retrieve and verify
369
+ const retrieved = await service.get(BigIntModel, created.id);
370
+ assert.strictEqual(retrieved.largeNumber, 9007199254740991n);
371
+ assert.strictEqual(retrieved.optionalBigInt, 1234567890123456789n);
372
+
373
+ // Update with new bigint value
374
+ const updated = await service.update(BigIntModel, BigIntModel.from({
375
+ id: created.id,
376
+ largeNumber: 18014398509481982n,
377
+ optionalBigInt: undefined
378
+ }));
379
+
380
+ assert.strictEqual(updated.largeNumber, 18014398509481982n);
381
+ assert.strictEqual(updated.optionalBigInt, undefined);
382
+
383
+ // Verify update persisted
384
+ const final = await service.get(BigIntModel, created.id);
385
+ assert.strictEqual(final.largeNumber, 18014398509481982n);
386
+ assert(!final.optionalBigInt);
387
+ }
346
388
  }
@@ -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 { type TimeSpan, type TimeUnit, TimeUtil } from '@travetto/runtime';
5
+ import { type TimeSpan, TimeUtil } from '@travetto/runtime';
6
6
 
7
7
  import { ExpiresAt, Model } from '../../src/registry/decorator.ts';
8
8
  import type { ModelExpirySupport } from '../../src/types/expiry.ts';
@@ -23,12 +23,12 @@ export abstract class ModelExpirySuite extends BaseModelSuite<ModelExpirySupport
23
23
 
24
24
  delayFactor: number = 1;
25
25
 
26
- async wait(n: number | TimeSpan) {
27
- await timers.setTimeout(TimeUtil.asMillis(n) * this.delayFactor);
26
+ async wait(input: TimeSpan | number | string) {
27
+ await timers.setTimeout(TimeUtil.duration(input, 'ms') * this.delayFactor);
28
28
  }
29
29
 
30
- timeFromNow(v: number | TimeSpan, unit?: TimeUnit) {
31
- return TimeUtil.fromNow(TimeUtil.asMillis(v, unit) * this.delayFactor);
30
+ timeFromNow(input: TimeSpan | number | string) {
31
+ return TimeUtil.fromNow(TimeUtil.duration(input, 'ms') * this.delayFactor);
32
32
  }
33
33
 
34
34
  @Test()
@@ -1,14 +1,80 @@
1
- import type { Class } from '@travetto/runtime';
1
+ import { type Class } from '@travetto/runtime';
2
2
  import { DependencyRegistryIndex } from '@travetto/di';
3
3
  import { Registry } from '@travetto/registry';
4
- import { SuiteRegistryIndex, TestFixtures } from '@travetto/test';
4
+ import { SuiteRegistryIndex, TestFixtures, type SuitePhaseHandler } from '@travetto/test';
5
5
  import { SchemaRegistryIndex } from '@travetto/schema';
6
6
 
7
7
  import { ModelBlobUtil } from '../../src/util/blob.ts';
8
8
  import { ModelStorageUtil } from '../../src/util/storage.ts';
9
9
  import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
10
10
 
11
- const Loaded = Symbol();
11
+ type ConfigType = { autoCreate?: boolean, namespace?: string };
12
+
13
+ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass: Class }> implements SuitePhaseHandler<T> {
14
+ qualifier?: symbol;
15
+ target: Class<T>;
16
+ constructor(target: Class<T>, qualifier?: symbol) {
17
+ this.qualifier = qualifier;
18
+ this.target = target;
19
+ }
20
+
21
+ async beforeAll(instance: T) {
22
+ await Registry.init();
23
+
24
+ const config = await DependencyRegistryIndex.getInstance<ConfigType>(instance.configClass);
25
+ if ('namespace' in config) {
26
+ config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
27
+ }
28
+
29
+ // We manually create
30
+ config.autoCreate = false;
31
+ }
32
+
33
+ async beforeEach(instance: T) {
34
+ const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
35
+ if (ModelStorageUtil.isSupported(service)) {
36
+ await service.createStorage();
37
+ if (service.upsertModel) {
38
+ await Promise.all(ModelRegistryIndex.getClasses()
39
+ .filter(cls => cls === SchemaRegistryIndex.getBaseClass(cls))
40
+ .map(modelCls => service.upsertModel!(modelCls)));
41
+ }
42
+ }
43
+ }
44
+
45
+ async afterEach(instance: T) {
46
+ const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
47
+ if (ModelStorageUtil.isSupported(service)) {
48
+ const models = ModelRegistryIndex.getClasses().filter(m => m === SchemaRegistryIndex.getBaseClass(m));
49
+
50
+ if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
51
+ await service.truncateBlob();
52
+ }
53
+
54
+ if (service.truncateModel) {
55
+ await Promise.all(models.map(x => service.truncateModel!(x)));
56
+ } else if (service.deleteModel) {
57
+ await Promise.all(models.map(x => service.deleteModel!(x)));
58
+ } else {
59
+ await service.deleteStorage(); // Purge it all
60
+ }
61
+ }
62
+ }
63
+
64
+ async afterAll(instance: T) {
65
+ const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
66
+ if (ModelStorageUtil.isSupported(service)) {
67
+ if (service.deleteModel) {
68
+ for (const model of ModelRegistryIndex.getClasses()) {
69
+ if (model === SchemaRegistryIndex.getBaseClass(model)) {
70
+ await service.deleteModel(model);
71
+ }
72
+ }
73
+ }
74
+ await service.deleteStorage();
75
+ }
76
+ }
77
+ }
12
78
 
13
79
  /**
14
80
  * Model test suite decorator
@@ -21,69 +87,7 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
21
87
  return (target: Class<T>): void => {
22
88
  target.prototype.fixtures = fixtures;
23
89
  SuiteRegistryIndex.getForRegister(target).register({
24
- beforeAll: [
25
- async function (this: T & { [Loaded]?: boolean }) {
26
- await Registry.init();
27
-
28
- if (!this[Loaded]) {
29
- const config = await DependencyRegistryIndex.getInstance(this.configClass);
30
- if ('namespace' in config) {
31
- config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
32
- }
33
- // We manually create
34
- config.autoCreate = false;
35
- this[Loaded] = true;
36
- }
37
- }
38
- ],
39
- beforeEach: [
40
- async function (this: T) {
41
- const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
42
- if (ModelStorageUtil.isSupported(service)) {
43
- await service.createStorage();
44
- if (service.upsertModel) {
45
- await Promise.all(ModelRegistryIndex.getClasses()
46
- .filter(x => x === SchemaRegistryIndex.getBaseClass(x))
47
- .map(m => service.upsertModel!(m)));
48
- }
49
- }
50
- }
51
- ],
52
- afterEach: [
53
- async function (this: T) {
54
- const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
55
- if (ModelStorageUtil.isSupported(service)) {
56
- const models = ModelRegistryIndex.getClasses().filter(m => m === SchemaRegistryIndex.getBaseClass(m));
57
-
58
- if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
59
- await service.truncateBlob();
60
- }
61
-
62
- if (service.truncateModel) {
63
- await Promise.all(models.map(x => service.truncateModel!(x)));
64
- } else if (service.deleteModel) {
65
- await Promise.all(models.map(x => service.deleteModel!(x)));
66
- } else {
67
- await service.deleteStorage(); // Purge it all
68
- }
69
- }
70
- }
71
- ],
72
- afterAll: [
73
- async function (this: T) {
74
- const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
75
- if (ModelStorageUtil.isSupported(service)) {
76
- if (service.deleteModel) {
77
- for (const m of ModelRegistryIndex.getClasses()) {
78
- if (m === SchemaRegistryIndex.getBaseClass(m)) {
79
- await service.deleteModel(m);
80
- }
81
- }
82
- }
83
- await service.deleteStorage();
84
- }
85
- }
86
- ]
90
+ phaseHandlers: [new ModelSuiteHandler(target, qualifier)]
87
91
  });
88
92
  };
89
93
  }