@tmlmobilidade/go-providers-storage 20260723.1350.16
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/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/operations/batch-delete.d.ts +14 -0
- package/dist/operations/batch-delete.js +45 -0
- package/dist/operations/batch-upload.d.ts +28 -0
- package/dist/operations/batch-upload.js +45 -0
- package/dist/operations/copy.d.ts +11 -0
- package/dist/operations/copy.js +71 -0
- package/dist/operations/delete.d.ts +17 -0
- package/dist/operations/delete.js +43 -0
- package/dist/operations/exists.d.ts +13 -0
- package/dist/operations/exists.js +30 -0
- package/dist/operations/find-by-id.d.ts +9 -0
- package/dist/operations/find-by-id.js +22 -0
- package/dist/operations/find-many.d.ts +13 -0
- package/dist/operations/find-many.js +16 -0
- package/dist/operations/get-signed-url.d.ts +9 -0
- package/dist/operations/get-signed-url.js +29 -0
- package/dist/operations/index.d.ts +12 -0
- package/dist/operations/index.js +12 -0
- package/dist/operations/move.d.ts +11 -0
- package/dist/operations/move.js +98 -0
- package/dist/operations/replace.d.ts +17 -0
- package/dist/operations/replace.js +99 -0
- package/dist/operations/upload.d.ts +13 -0
- package/dist/operations/upload.js +52 -0
- package/dist/operations/validate-upload.d.ts +15 -0
- package/dist/operations/validate-upload.js +28 -0
- package/dist/provider.d.ts +130 -0
- package/dist/provider.js +146 -0
- package/dist/types/blob-body.d.ts +2 -0
- package/dist/types/blob-body.js +1 -0
- package/dist/types/deps.d.ts +6 -0
- package/dist/types/deps.js +1 -0
- package/dist/types/hooks.d.ts +8 -0
- package/dist/types/hooks.js +1 -0
- package/dist/types/operation-context.d.ts +8 -0
- package/dist/types/operation-context.js +1 -0
- package/dist/utils/mime.d.ts +23 -0
- package/dist/utils/mime.js +41 -0
- package/dist/utils/observability.d.ts +35 -0
- package/dist/utils/observability.js +46 -0
- package/dist/utils/operation-runner.d.ts +51 -0
- package/dist/utils/operation-runner.js +124 -0
- package/dist/utils/storage-key.d.ts +22 -0
- package/dist/utils/storage-key.js +29 -0
- package/package.json +56 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
2
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
3
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
4
|
+
import { type BatchResult } from './batch-upload.js';
|
|
5
|
+
export interface BatchDeleteInput {
|
|
6
|
+
concurrency?: number;
|
|
7
|
+
fileIds: string[];
|
|
8
|
+
hooks?: OperationHooks<OperationContext, BatchResult<{
|
|
9
|
+
fileId: string;
|
|
10
|
+
}>>;
|
|
11
|
+
}
|
|
12
|
+
export declare function batchDelete(deps: StorageDeps, input: BatchDeleteInput): Promise<BatchResult<{
|
|
13
|
+
fileId: string;
|
|
14
|
+
}>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { toStorageError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
3
|
+
import { runWithConcurrency } from '@tmlmobilidade/utils';
|
|
4
|
+
import { deleteAttachment } from './delete.js';
|
|
5
|
+
/* * */
|
|
6
|
+
export async function batchDelete(deps, input) {
|
|
7
|
+
//
|
|
8
|
+
const { concurrency = 5, fileIds, hooks } = input;
|
|
9
|
+
const context = { itemCount: fileIds.length, operation: 'batchDelete' };
|
|
10
|
+
await hooks?.onStart?.(context);
|
|
11
|
+
try {
|
|
12
|
+
const settlements = await runWithConcurrency(fileIds, concurrency, async (fileId) => {
|
|
13
|
+
return deleteAttachment(deps, {
|
|
14
|
+
fileId,
|
|
15
|
+
hooks: {
|
|
16
|
+
onError: async () => undefined,
|
|
17
|
+
onSuccess: async () => undefined,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const fulfilled = [];
|
|
22
|
+
const rejected = [];
|
|
23
|
+
settlements.forEach((settlement, index) => {
|
|
24
|
+
if (settlement.status === 'fulfilled') {
|
|
25
|
+
fulfilled.push({ index, value: settlement.value });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
rejected.push({ error: settlement.reason, index });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const result = { fulfilled, rejected };
|
|
32
|
+
await hooks?.onSuccess?.(context, result);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
//
|
|
37
|
+
const storageError = toStorageError(error, { operation: context.operation });
|
|
38
|
+
await hooks?.onError?.(context, storageError);
|
|
39
|
+
throw storageError;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
//
|
|
43
|
+
await hooks?.onFinally?.(context);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BlobBody } from '../types/blob-body.js';
|
|
2
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
3
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
4
|
+
import { type StorageError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
5
|
+
import { type Attachment, type CreateAttachmentDto } from '@tmlmobilidade/types';
|
|
6
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
7
|
+
export interface BatchUploadItem {
|
|
8
|
+
createAttachmentDto: CreateAttachmentDto & {
|
|
9
|
+
_id?: string;
|
|
10
|
+
};
|
|
11
|
+
file: BlobBody;
|
|
12
|
+
}
|
|
13
|
+
export interface BatchResult<T> {
|
|
14
|
+
fulfilled: {
|
|
15
|
+
index: number;
|
|
16
|
+
value: T;
|
|
17
|
+
}[];
|
|
18
|
+
rejected: {
|
|
19
|
+
error: StorageError;
|
|
20
|
+
index: number;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
export interface BatchUploadInput {
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
hooks?: OperationHooks<OperationContext, BatchResult<Attachment>>;
|
|
26
|
+
items: BatchUploadItem[];
|
|
27
|
+
}
|
|
28
|
+
export declare function batchUpload(deps: StorageDeps, input: BatchUploadInput): Promise<BatchResult<Attachment>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { toStorageError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
3
|
+
import { runWithConcurrency } from '@tmlmobilidade/utils';
|
|
4
|
+
import { upload } from './upload.js';
|
|
5
|
+
export async function batchUpload(deps, input) {
|
|
6
|
+
//
|
|
7
|
+
const { concurrency = 5, hooks, items } = input;
|
|
8
|
+
const context = { itemCount: items.length, operation: 'batchUpload' };
|
|
9
|
+
await hooks?.onStart?.(context);
|
|
10
|
+
try {
|
|
11
|
+
const settlements = await runWithConcurrency(items, concurrency, async (item) => {
|
|
12
|
+
return upload(deps, {
|
|
13
|
+
createAttachmentDto: item.createAttachmentDto,
|
|
14
|
+
file: item.file,
|
|
15
|
+
hooks: {
|
|
16
|
+
onError: async () => undefined,
|
|
17
|
+
onSuccess: async () => undefined,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const fulfilled = [];
|
|
22
|
+
const rejected = [];
|
|
23
|
+
settlements.forEach((settlement, index) => {
|
|
24
|
+
if (settlement.status === 'fulfilled') {
|
|
25
|
+
fulfilled.push({ index, value: settlement.value });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
rejected.push({ error: settlement.reason, index });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const result = { fulfilled, rejected };
|
|
32
|
+
await hooks?.onSuccess?.(context, result);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
//
|
|
37
|
+
const storageError = toStorageError(error, { operation: context.operation });
|
|
38
|
+
await hooks?.onError?.(context, storageError);
|
|
39
|
+
throw storageError;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
//
|
|
43
|
+
await hooks?.onFinally?.(context);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
2
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
3
|
+
import { type Attachment } from '@tmlmobilidade/types';
|
|
4
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
5
|
+
export interface CopyInput {
|
|
6
|
+
fileId: string;
|
|
7
|
+
hooks?: OperationHooks<OperationContext, Attachment>;
|
|
8
|
+
resourceId: string;
|
|
9
|
+
scope: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function copy(deps: StorageDeps, input: CopyInput): Promise<Attachment>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { getFileExtension } from '../utils/mime.js';
|
|
3
|
+
import { runSaga } from '../utils/operation-runner.js';
|
|
4
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
5
|
+
import { MetadataError, NotFoundError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
6
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
7
|
+
import { generateRandomString } from '@tmlmobilidade/strings';
|
|
8
|
+
import { CreateAttachmentSchema } from '@tmlmobilidade/types';
|
|
9
|
+
import { convertObject } from '@tmlmobilidade/utils';
|
|
10
|
+
/* * */
|
|
11
|
+
export async function copy(deps, input) {
|
|
12
|
+
//
|
|
13
|
+
const { fileId, hooks, resourceId, scope } = input;
|
|
14
|
+
const _id = generateRandomString({ length: 5 });
|
|
15
|
+
const context = { attachmentId: _id, operation: 'copy', resourceId, scope, sourceAttachmentId: fileId };
|
|
16
|
+
let source;
|
|
17
|
+
let originalFilePath = '';
|
|
18
|
+
let newFilePath = '';
|
|
19
|
+
let inserted;
|
|
20
|
+
return runSaga({
|
|
21
|
+
context,
|
|
22
|
+
hooks,
|
|
23
|
+
observability: deps.observability,
|
|
24
|
+
result: () => {
|
|
25
|
+
if (!inserted)
|
|
26
|
+
throw new MetadataError('Copy completed without inserted attachment');
|
|
27
|
+
return inserted;
|
|
28
|
+
},
|
|
29
|
+
steps: [
|
|
30
|
+
{
|
|
31
|
+
execute: async () => {
|
|
32
|
+
const found = await goDb.core.attachments.findOne({ _id: { $eq: fileId } });
|
|
33
|
+
if (!found)
|
|
34
|
+
throw new NotFoundError('File not found', { context: { fileId } });
|
|
35
|
+
source = found;
|
|
36
|
+
originalFilePath = storageKey(found);
|
|
37
|
+
newFilePath = `${scope}/${resourceId}/${_id}.${getFileExtension(found.name)}`;
|
|
38
|
+
context.key = newFilePath;
|
|
39
|
+
},
|
|
40
|
+
name: 'loadSource',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
compensate: async () => {
|
|
44
|
+
await deps.blobs.deleteFile(newFilePath);
|
|
45
|
+
},
|
|
46
|
+
execute: async () => {
|
|
47
|
+
await deps.blobs.copyFile(originalFilePath, newFilePath);
|
|
48
|
+
},
|
|
49
|
+
name: 'copyBlob',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
compensate: async () => {
|
|
53
|
+
if (inserted)
|
|
54
|
+
await goDb.core.attachments.deleteById(inserted._id, { forceIfLocked: true });
|
|
55
|
+
},
|
|
56
|
+
execute: async () => {
|
|
57
|
+
if (!source)
|
|
58
|
+
throw new MetadataError('Copy source missing during insert');
|
|
59
|
+
const newFile = convertObject(source, CreateAttachmentSchema);
|
|
60
|
+
inserted = await goDb.core.attachments.insertOne({
|
|
61
|
+
...newFile,
|
|
62
|
+
_id,
|
|
63
|
+
resource_id: resourceId,
|
|
64
|
+
scope,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
name: 'insertMetadata',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
2
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
3
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
4
|
+
/**
|
|
5
|
+
* Deletes blob then metadata.
|
|
6
|
+
* Invariant: prefer an orphan blob over dangling metadata pointing at a missing blob.
|
|
7
|
+
* If metadata delete fails after blob delete, there is no blob restore compensation.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeleteInput {
|
|
10
|
+
fileId: string;
|
|
11
|
+
hooks?: OperationHooks<OperationContext, {
|
|
12
|
+
fileId: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare function deleteAttachment(deps: StorageDeps, input: DeleteInput): Promise<{
|
|
16
|
+
fileId: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { runSaga } from '../utils/operation-runner.js';
|
|
3
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
4
|
+
import { NotFoundError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
5
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
6
|
+
export async function deleteAttachment(deps, input) {
|
|
7
|
+
//
|
|
8
|
+
const { fileId, hooks } = input;
|
|
9
|
+
const context = { attachmentId: fileId, operation: 'delete' };
|
|
10
|
+
let key = '';
|
|
11
|
+
return runSaga({
|
|
12
|
+
context,
|
|
13
|
+
hooks,
|
|
14
|
+
observability: deps.observability,
|
|
15
|
+
result: () => ({ fileId }),
|
|
16
|
+
steps: [
|
|
17
|
+
{
|
|
18
|
+
execute: async () => {
|
|
19
|
+
const attachment = await goDb.core.attachments.findById(fileId);
|
|
20
|
+
if (!attachment)
|
|
21
|
+
throw new NotFoundError('File not found', { context: { fileId } });
|
|
22
|
+
key = storageKey(attachment);
|
|
23
|
+
context.key = key;
|
|
24
|
+
context.resourceId = attachment.resource_id;
|
|
25
|
+
context.scope = attachment.scope;
|
|
26
|
+
},
|
|
27
|
+
name: 'loadSource',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
execute: async () => {
|
|
31
|
+
await deps.blobs.deleteFile(key);
|
|
32
|
+
},
|
|
33
|
+
name: 'deleteBlob',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
execute: async () => {
|
|
37
|
+
await goDb.core.attachments.deleteById(fileId, { forceIfLocked: true });
|
|
38
|
+
},
|
|
39
|
+
name: 'deleteMetadata',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
2
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
3
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
4
|
+
export interface ExistsResult {
|
|
5
|
+
blob: boolean;
|
|
6
|
+
metadata: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ExistsInput {
|
|
9
|
+
fileId?: string;
|
|
10
|
+
hooks?: OperationHooks<OperationContext, ExistsResult>;
|
|
11
|
+
key?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function exists(deps: StorageDeps, input: ExistsInput): Promise<ExistsResult>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { runOperation } from '../utils/operation-runner.js';
|
|
3
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
4
|
+
import { ValidationError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
5
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
6
|
+
/* * */
|
|
7
|
+
export async function exists(deps, input) {
|
|
8
|
+
//
|
|
9
|
+
const { fileId, hooks, key: keyInput } = input;
|
|
10
|
+
const context = { attachmentId: fileId, key: keyInput, operation: 'exists' };
|
|
11
|
+
return runOperation({
|
|
12
|
+
context,
|
|
13
|
+
execute: async () => {
|
|
14
|
+
if (!fileId && !keyInput) {
|
|
15
|
+
throw new ValidationError('Either "fileId" or "key" must be provided');
|
|
16
|
+
}
|
|
17
|
+
if (fileId) {
|
|
18
|
+
const file = await goDb.core.attachments.findById(fileId);
|
|
19
|
+
if (!file)
|
|
20
|
+
return { blob: false, metadata: false };
|
|
21
|
+
const blob = await deps.blobs.fileExists(storageKey(file));
|
|
22
|
+
return { blob, metadata: true };
|
|
23
|
+
}
|
|
24
|
+
const blob = await deps.blobs.fileExists(keyInput);
|
|
25
|
+
return { blob, metadata: false };
|
|
26
|
+
},
|
|
27
|
+
hooks,
|
|
28
|
+
observability: deps.observability,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
2
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
3
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
4
|
+
import { type Attachment } from '@tmlmobilidade/types';
|
|
5
|
+
export interface FindByIdInput {
|
|
6
|
+
hooks?: OperationHooks<OperationContext, Attachment | null>;
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function findById(deps: StorageDeps, input: FindByIdInput): Promise<Attachment | null>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { runOperation } from '../utils/operation-runner.js';
|
|
3
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
4
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
5
|
+
/* * */
|
|
6
|
+
export async function findById(deps, input) {
|
|
7
|
+
//
|
|
8
|
+
const { hooks, id } = input;
|
|
9
|
+
const context = { attachmentId: id, operation: 'findById' };
|
|
10
|
+
return runOperation({
|
|
11
|
+
context,
|
|
12
|
+
execute: async () => {
|
|
13
|
+
const file = await goDb.core.attachments.findById(id);
|
|
14
|
+
if (!file)
|
|
15
|
+
return null;
|
|
16
|
+
file.url = await deps.blobs.getFileUrl(storageKey(file));
|
|
17
|
+
return file;
|
|
18
|
+
},
|
|
19
|
+
hooks,
|
|
20
|
+
observability: deps.observability,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StorageDeps } from '../types/deps.js';
|
|
2
|
+
import type { OperationHooks } from '../types/hooks.js';
|
|
3
|
+
import type { OperationContext } from '../types/operation-context.js';
|
|
4
|
+
import type { Filter, FindOptions } from '@tmlmobilidade/go-clients-mongo';
|
|
5
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
6
|
+
import { Attachment } from '@tmlmobilidade/types';
|
|
7
|
+
interface FindManyInput {
|
|
8
|
+
filter?: Filter<Attachment>;
|
|
9
|
+
hooks?: OperationHooks<OperationContext, Attachment[]>;
|
|
10
|
+
options?: FindOptions;
|
|
11
|
+
}
|
|
12
|
+
export declare function findMany(deps: StorageDeps, input: FindManyInput): ReturnType<typeof goDb.core.attachments.findMany>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { runOperation } from '../utils/operation-runner.js';
|
|
3
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
4
|
+
export async function findMany(deps, input) {
|
|
5
|
+
//
|
|
6
|
+
const { filter, hooks, options } = input;
|
|
7
|
+
const context = { operation: 'findMany' };
|
|
8
|
+
return runOperation({
|
|
9
|
+
context,
|
|
10
|
+
execute: async () => {
|
|
11
|
+
return await goDb.core.attachments.findMany(filter, options);
|
|
12
|
+
},
|
|
13
|
+
hooks,
|
|
14
|
+
observability: deps.observability,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
2
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
3
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
4
|
+
export interface GetSignedUrlInput {
|
|
5
|
+
fileId?: string;
|
|
6
|
+
hooks?: OperationHooks<OperationContext, string>;
|
|
7
|
+
key?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function getSignedUrl(deps: StorageDeps, input: GetSignedUrlInput): Promise<string>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { runOperation } from '../utils/operation-runner.js';
|
|
3
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
4
|
+
import { NotFoundError, ValidationError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
5
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
6
|
+
/* * */
|
|
7
|
+
export async function getSignedUrl(deps, input) {
|
|
8
|
+
//
|
|
9
|
+
const { fileId, hooks, key: keyInput } = input;
|
|
10
|
+
const context = { attachmentId: fileId, key: keyInput, operation: 'getSignedUrl' };
|
|
11
|
+
return runOperation({
|
|
12
|
+
context,
|
|
13
|
+
execute: async () => {
|
|
14
|
+
if (!fileId && !keyInput) {
|
|
15
|
+
throw new ValidationError('Either "fileId" or "key" must be provided');
|
|
16
|
+
}
|
|
17
|
+
let key = keyInput;
|
|
18
|
+
if (fileId) {
|
|
19
|
+
const file = await goDb.core.attachments.findOne({ _id: { $eq: fileId } });
|
|
20
|
+
if (!file)
|
|
21
|
+
throw new NotFoundError('File not found', { context: { fileId } });
|
|
22
|
+
key = storageKey(file);
|
|
23
|
+
}
|
|
24
|
+
return deps.blobs.getFileUrl(key);
|
|
25
|
+
},
|
|
26
|
+
hooks,
|
|
27
|
+
observability: deps.observability,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './batch-delete.js';
|
|
2
|
+
export * from './batch-upload.js';
|
|
3
|
+
export * from './copy.js';
|
|
4
|
+
export * from './delete.js';
|
|
5
|
+
export * from './exists.js';
|
|
6
|
+
export * from './find-by-id.js';
|
|
7
|
+
export * from './find-many.js';
|
|
8
|
+
export * from './get-signed-url.js';
|
|
9
|
+
export * from './move.js';
|
|
10
|
+
export * from './replace.js';
|
|
11
|
+
export * from './upload.js';
|
|
12
|
+
export * from './validate-upload.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './batch-delete.js';
|
|
2
|
+
export * from './batch-upload.js';
|
|
3
|
+
export * from './copy.js';
|
|
4
|
+
export * from './delete.js';
|
|
5
|
+
export * from './exists.js';
|
|
6
|
+
export * from './find-by-id.js';
|
|
7
|
+
export * from './find-many.js';
|
|
8
|
+
export * from './get-signed-url.js';
|
|
9
|
+
export * from './move.js';
|
|
10
|
+
export * from './replace.js';
|
|
11
|
+
export * from './upload.js';
|
|
12
|
+
export * from './validate-upload.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
2
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
3
|
+
import { type Attachment } from '@tmlmobilidade/types';
|
|
4
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
5
|
+
export interface MoveInput {
|
|
6
|
+
fileId: string;
|
|
7
|
+
hooks?: OperationHooks<OperationContext, Attachment>;
|
|
8
|
+
resourceId: string;
|
|
9
|
+
scope: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function move(deps: StorageDeps, input: MoveInput): Promise<Attachment>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { getFileExtension } from '../utils/mime.js';
|
|
3
|
+
import { runSaga } from '../utils/operation-runner.js';
|
|
4
|
+
import { storageKey } from '../utils/storage-key.js';
|
|
5
|
+
import { MetadataError, NotFoundError } from '@tmlmobilidade/go-clients-oci-storage';
|
|
6
|
+
import { goDb } from '@tmlmobilidade/go-interfaces-godb';
|
|
7
|
+
import { generateRandomString } from '@tmlmobilidade/strings';
|
|
8
|
+
import { CreateAttachmentSchema } from '@tmlmobilidade/types';
|
|
9
|
+
import { convertObject } from '@tmlmobilidade/utils';
|
|
10
|
+
/* * */
|
|
11
|
+
export async function move(deps, input) {
|
|
12
|
+
//
|
|
13
|
+
const { fileId, hooks, resourceId, scope } = input;
|
|
14
|
+
const _id = generateRandomString({ length: 5 });
|
|
15
|
+
const context = { attachmentId: _id, operation: 'move', resourceId, scope, sourceAttachmentId: fileId };
|
|
16
|
+
let source;
|
|
17
|
+
let originalFilePath = '';
|
|
18
|
+
let newFilePath = '';
|
|
19
|
+
let inserted;
|
|
20
|
+
let sourceBlobDeleted = false;
|
|
21
|
+
let sourceMetaDeleted = false;
|
|
22
|
+
return runSaga({
|
|
23
|
+
context,
|
|
24
|
+
hooks,
|
|
25
|
+
observability: deps.observability,
|
|
26
|
+
result: () => {
|
|
27
|
+
if (!inserted)
|
|
28
|
+
throw new MetadataError('Move completed without inserted attachment');
|
|
29
|
+
return inserted;
|
|
30
|
+
},
|
|
31
|
+
steps: [
|
|
32
|
+
{
|
|
33
|
+
execute: async () => {
|
|
34
|
+
const found = await goDb.core.attachments.findOne({ _id: { $eq: fileId } });
|
|
35
|
+
if (!found)
|
|
36
|
+
throw new NotFoundError('File not found', { context: { fileId } });
|
|
37
|
+
source = found;
|
|
38
|
+
originalFilePath = storageKey(found);
|
|
39
|
+
newFilePath = `${scope}/${resourceId}/${_id}.${getFileExtension(found.name)}`;
|
|
40
|
+
context.key = newFilePath;
|
|
41
|
+
},
|
|
42
|
+
name: 'loadSource',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
compensate: async () => {
|
|
46
|
+
await deps.blobs.deleteFile(newFilePath);
|
|
47
|
+
},
|
|
48
|
+
execute: async () => {
|
|
49
|
+
await deps.blobs.copyFile(originalFilePath, newFilePath);
|
|
50
|
+
},
|
|
51
|
+
name: 'copyBlob',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
compensate: async () => {
|
|
55
|
+
if (inserted)
|
|
56
|
+
await goDb.core.attachments.deleteById(inserted._id, { forceIfLocked: true });
|
|
57
|
+
},
|
|
58
|
+
execute: async () => {
|
|
59
|
+
if (!source)
|
|
60
|
+
throw new MetadataError('Move source missing during insert');
|
|
61
|
+
const newFile = convertObject(source, CreateAttachmentSchema);
|
|
62
|
+
inserted = await goDb.core.attachments.insertOne({
|
|
63
|
+
...newFile,
|
|
64
|
+
_id,
|
|
65
|
+
resource_id: resourceId,
|
|
66
|
+
scope,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
name: 'insertMetadata',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
compensate: async () => {
|
|
73
|
+
if (sourceBlobDeleted) {
|
|
74
|
+
await deps.blobs.copyFile(newFilePath, originalFilePath);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
execute: async () => {
|
|
78
|
+
await deps.blobs.deleteFile(originalFilePath);
|
|
79
|
+
sourceBlobDeleted = true;
|
|
80
|
+
},
|
|
81
|
+
name: 'deleteSourceBlob',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
compensate: async () => {
|
|
85
|
+
if (sourceMetaDeleted && source) {
|
|
86
|
+
const restored = convertObject(source, CreateAttachmentSchema);
|
|
87
|
+
await goDb.core.attachments.insertOne({ ...restored, _id: fileId });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
execute: async () => {
|
|
91
|
+
await goDb.core.attachments.deleteById(fileId, { forceIfLocked: true });
|
|
92
|
+
sourceMetaDeleted = true;
|
|
93
|
+
},
|
|
94
|
+
name: 'deleteSourceMetadata',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BlobBody } from '../types/blob-body.js';
|
|
2
|
+
import { type StorageDeps } from '../types/deps.js';
|
|
3
|
+
import { type OperationHooks } from '../types/hooks.js';
|
|
4
|
+
import { type OperationContext } from '../types/operation-context.js';
|
|
5
|
+
import { type Attachment, type CreateAttachmentDto } from '@tmlmobilidade/types';
|
|
6
|
+
/**
|
|
7
|
+
* Replace an existing attachment in place (same id / path).
|
|
8
|
+
* Uses copy-aside of the old blob so a failed put can restore the previous object.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReplaceInput {
|
|
11
|
+
createAttachmentDto: CreateAttachmentDto & {
|
|
12
|
+
_id: string;
|
|
13
|
+
};
|
|
14
|
+
file: BlobBody;
|
|
15
|
+
hooks?: OperationHooks<OperationContext, Attachment>;
|
|
16
|
+
}
|
|
17
|
+
export declare function replace(deps: StorageDeps, input: ReplaceInput): Promise<Attachment>;
|