@travetto/model 5.0.0-rc.12 → 5.0.0-rc.13
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/package.json +7 -7
- package/src/internal/service/blob.ts +46 -2
- package/src/service/blob.ts +4 -9
- package/support/test/blob.ts +29 -10
- package/src/util/blob.ts +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.13",
|
|
4
4
|
"description": "Datastore abstraction for core operations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/model"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^5.0.0-rc.
|
|
30
|
-
"@travetto/di": "^5.0.0-rc.
|
|
31
|
-
"@travetto/registry": "^5.0.0-rc.
|
|
32
|
-
"@travetto/schema": "^5.0.0-rc.
|
|
29
|
+
"@travetto/config": "^5.0.0-rc.13",
|
|
30
|
+
"@travetto/di": "^5.0.0-rc.12",
|
|
31
|
+
"@travetto/registry": "^5.0.0-rc.12",
|
|
32
|
+
"@travetto/schema": "^5.0.0-rc.13"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^5.0.0-rc.
|
|
36
|
-
"@travetto/test": "^5.0.0-rc.
|
|
35
|
+
"@travetto/cli": "^5.0.0-rc.13",
|
|
36
|
+
"@travetto/test": "^5.0.0-rc.12"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
|
@@ -1,5 +1,49 @@
|
|
|
1
|
-
import { Class } from '@travetto/runtime';
|
|
1
|
+
import { Class, AppError, BinaryInput, BinaryUtil, BlobMeta, ByteRange } from '@travetto/runtime';
|
|
2
2
|
import { ModelType } from '../../types/model';
|
|
3
3
|
|
|
4
4
|
export const ModelBlobNamespace = '__blobs';
|
|
5
|
-
export const MODEL_BLOB: Class<ModelType> = class { id: string; };
|
|
5
|
+
export const MODEL_BLOB: Class<ModelType> = class { id: string; };
|
|
6
|
+
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utilities for processing assets
|
|
12
|
+
*/
|
|
13
|
+
export class ModelBlobUtil {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert input to a Readable, and get what metadata is available
|
|
17
|
+
*/
|
|
18
|
+
static async getInput(src: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
|
|
19
|
+
let input: Readable;
|
|
20
|
+
if (src instanceof Blob) {
|
|
21
|
+
metadata = { ...BinaryUtil.getBlobMeta(src), ...metadata };
|
|
22
|
+
metadata.size ??= src.size;
|
|
23
|
+
input = Readable.fromWeb(src.stream());
|
|
24
|
+
} else if (typeof src === 'object' && 'pipeThrough' in src) {
|
|
25
|
+
input = Readable.fromWeb(src);
|
|
26
|
+
} else if (typeof src === 'object' && 'pipe' in src) {
|
|
27
|
+
input = src;
|
|
28
|
+
} else {
|
|
29
|
+
metadata.size = src.length;
|
|
30
|
+
input = Readable.from(src);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [input, 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', 'data');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { start, end };
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/service/blob.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { BinaryInput, BlobMeta, ByteRange } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Support for Blobs
|
|
4
|
+
* Support for Blobs CRUD.
|
|
5
5
|
*
|
|
6
6
|
* @concrete ../internal/service/common#ModelBlobSupportTarget
|
|
7
7
|
*/
|
|
8
8
|
export interface ModelBlobSupport {
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Insert blob to storage
|
|
12
|
-
* @param location The location of the blob
|
|
13
|
-
* @param input The actual blob to write
|
|
14
|
-
*/
|
|
15
|
-
insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting?: boolean): Promise<void>;
|
|
16
|
-
|
|
17
10
|
/**
|
|
18
11
|
* Upsert blob to storage
|
|
19
12
|
* @param location The location of the blob
|
|
20
13
|
* @param input The actual blob to write
|
|
14
|
+
* @param meta Additional metadata to store with the blob
|
|
15
|
+
* @param overwrite Should we replace content if already found, defaults to true
|
|
21
16
|
*/
|
|
22
|
-
upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void>;
|
|
17
|
+
upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite?: boolean): Promise<void>;
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
20
|
* Get blob from storage
|
package/support/test/blob.ts
CHANGED
|
@@ -2,10 +2,12 @@ import assert from 'node:assert';
|
|
|
2
2
|
|
|
3
3
|
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
4
4
|
import { BaseModelSuite } from '@travetto/model/support/test/base';
|
|
5
|
-
import { Util } from '@travetto/runtime';
|
|
5
|
+
import { BinaryUtil, Util } from '@travetto/runtime';
|
|
6
6
|
|
|
7
7
|
import { ModelBlobSupport } from '../../src/service/blob';
|
|
8
|
-
import { ModelBlobUtil } from '../../src/
|
|
8
|
+
import { ModelBlobUtil } from '../../src/internal/service/blob';
|
|
9
|
+
|
|
10
|
+
const meta = BinaryUtil.getBlobMeta;
|
|
9
11
|
|
|
10
12
|
@Suite()
|
|
11
13
|
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
@@ -20,9 +22,26 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
20
22
|
const id = Util.uuid();
|
|
21
23
|
|
|
22
24
|
await service.upsertBlob(id, buffer);
|
|
23
|
-
const
|
|
25
|
+
const m = await service.describeBlob(id);
|
|
24
26
|
const retrieved = await service.describeBlob(id);
|
|
25
|
-
assert.deepStrictEqual(
|
|
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');
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
@Test()
|
|
@@ -32,11 +51,11 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
32
51
|
|
|
33
52
|
const id = Util.uuid();
|
|
34
53
|
await service.upsertBlob(id, buffer);
|
|
35
|
-
const
|
|
54
|
+
const { hash } = await service.describeBlob(id);
|
|
36
55
|
|
|
37
56
|
const retrieved = await service.getBlob(id);
|
|
38
|
-
const
|
|
39
|
-
assert(
|
|
57
|
+
const { hash: received } = meta(retrieved)!;
|
|
58
|
+
assert(hash === received);
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
@Test()
|
|
@@ -69,7 +88,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
69
88
|
|
|
70
89
|
const partial = await service.getBlob(id, { start: 10, end: 20 });
|
|
71
90
|
assert(partial.size === 11);
|
|
72
|
-
const partialMeta = partial
|
|
91
|
+
const partialMeta = meta(partial)!;
|
|
73
92
|
const subContent = await partial.text();
|
|
74
93
|
const range = await ModelBlobUtil.enforceRange({ start: 10, end: 20 }, partialMeta.size!);
|
|
75
94
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
@@ -79,7 +98,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
79
98
|
assert(subContent === og.substring(10, 21));
|
|
80
99
|
|
|
81
100
|
const partialUnbounded = await service.getBlob(id, { start: 10 });
|
|
82
|
-
const partialUnboundedMeta = partial
|
|
101
|
+
const partialUnboundedMeta = meta(partial)!;
|
|
83
102
|
const subContent2 = await partialUnbounded.text();
|
|
84
103
|
const range2 = await ModelBlobUtil.enforceRange({ start: 10 }, partialUnboundedMeta.size!);
|
|
85
104
|
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
@@ -107,7 +126,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
107
126
|
const buffer = await this.fixture.read('/asset.yml', true);
|
|
108
127
|
await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
|
|
109
128
|
const saved = await service.getBlob('orange');
|
|
110
|
-
const savedMeta = saved
|
|
129
|
+
const savedMeta = meta(saved)!;
|
|
111
130
|
|
|
112
131
|
assert('text/yaml' === savedMeta.contentType);
|
|
113
132
|
assert(buffer.length === savedMeta.size);
|
package/src/util/blob.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { AppError, BinaryInput, BlobMeta, ByteRange, Util } from '@travetto/runtime';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Utilities for processing assets
|
|
8
|
-
*/
|
|
9
|
-
export class ModelBlobUtil {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get a hashed location/path for a blob
|
|
13
|
-
*/
|
|
14
|
-
static getHashedLocation(meta: BlobMeta, prefix = ''): string {
|
|
15
|
-
const hash = meta.hash ?? Util.uuid();
|
|
16
|
-
|
|
17
|
-
let parts = hash.match(/(.{1,4})/g)!.slice();
|
|
18
|
-
if (parts.length > 4) {
|
|
19
|
-
parts = [...parts.slice(0, 4), parts.slice(4).join('')];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const ext = path.extname(meta.filename ?? '') || '.bin';
|
|
23
|
-
return `${parts.join('/')}${ext}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Convert input to a blob, containing all data in memory
|
|
28
|
-
*/
|
|
29
|
-
static async getInput(src: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
|
|
30
|
-
let input: Readable;
|
|
31
|
-
if (src instanceof Blob) {
|
|
32
|
-
metadata = { ...src.meta, ...metadata };
|
|
33
|
-
metadata.size ??= src.size;
|
|
34
|
-
input = Readable.fromWeb(src.stream());
|
|
35
|
-
} else if (typeof src === 'object' && 'pipeThrough' in src) {
|
|
36
|
-
input = Readable.fromWeb(src);
|
|
37
|
-
} else if (typeof src === 'object' && 'pipe' in src) {
|
|
38
|
-
input = src;
|
|
39
|
-
} else {
|
|
40
|
-
metadata.size = src.length;
|
|
41
|
-
input = Readable.from(src);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return [input, metadata ?? {}];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Enforce byte range for stream stream/file of a certain size
|
|
49
|
-
*/
|
|
50
|
-
static enforceRange({ start, end }: ByteRange, size: number): Required<ByteRange> {
|
|
51
|
-
// End is inclusive
|
|
52
|
-
end = Math.min(end ?? (size - 1), size - 1);
|
|
53
|
-
|
|
54
|
-
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
|
|
55
|
-
throw new AppError('Invalid position, out of range', 'data');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { start, end };
|
|
59
|
-
}
|
|
60
|
-
}
|