@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.
Files changed (47) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.js +3 -0
  3. package/dist/operations/batch-delete.d.ts +14 -0
  4. package/dist/operations/batch-delete.js +45 -0
  5. package/dist/operations/batch-upload.d.ts +28 -0
  6. package/dist/operations/batch-upload.js +45 -0
  7. package/dist/operations/copy.d.ts +11 -0
  8. package/dist/operations/copy.js +71 -0
  9. package/dist/operations/delete.d.ts +17 -0
  10. package/dist/operations/delete.js +43 -0
  11. package/dist/operations/exists.d.ts +13 -0
  12. package/dist/operations/exists.js +30 -0
  13. package/dist/operations/find-by-id.d.ts +9 -0
  14. package/dist/operations/find-by-id.js +22 -0
  15. package/dist/operations/find-many.d.ts +13 -0
  16. package/dist/operations/find-many.js +16 -0
  17. package/dist/operations/get-signed-url.d.ts +9 -0
  18. package/dist/operations/get-signed-url.js +29 -0
  19. package/dist/operations/index.d.ts +12 -0
  20. package/dist/operations/index.js +12 -0
  21. package/dist/operations/move.d.ts +11 -0
  22. package/dist/operations/move.js +98 -0
  23. package/dist/operations/replace.d.ts +17 -0
  24. package/dist/operations/replace.js +99 -0
  25. package/dist/operations/upload.d.ts +13 -0
  26. package/dist/operations/upload.js +52 -0
  27. package/dist/operations/validate-upload.d.ts +15 -0
  28. package/dist/operations/validate-upload.js +28 -0
  29. package/dist/provider.d.ts +130 -0
  30. package/dist/provider.js +146 -0
  31. package/dist/types/blob-body.d.ts +2 -0
  32. package/dist/types/blob-body.js +1 -0
  33. package/dist/types/deps.d.ts +6 -0
  34. package/dist/types/deps.js +1 -0
  35. package/dist/types/hooks.d.ts +8 -0
  36. package/dist/types/hooks.js +1 -0
  37. package/dist/types/operation-context.d.ts +8 -0
  38. package/dist/types/operation-context.js +1 -0
  39. package/dist/utils/mime.d.ts +23 -0
  40. package/dist/utils/mime.js +41 -0
  41. package/dist/utils/observability.d.ts +35 -0
  42. package/dist/utils/observability.js +46 -0
  43. package/dist/utils/operation-runner.d.ts +51 -0
  44. package/dist/utils/operation-runner.js +124 -0
  45. package/dist/utils/storage-key.d.ts +22 -0
  46. package/dist/utils/storage-key.js +29 -0
  47. package/package.json +56 -0
@@ -0,0 +1,46 @@
1
+ /* * */
2
+ import { Logger } from '@tmlmobilidade/logger';
3
+ /**
4
+ * No-op implementation of the Observability interface.
5
+ *
6
+ * @type {Observability}
7
+ * @property {function} onOperationEnd - Does nothing.
8
+ * @property {function} onOperationStart - Does nothing.
9
+ * @property {function} onStep - Does nothing.
10
+ */
11
+ export const noopObservability = {
12
+ onOperationEnd: () => undefined,
13
+ onOperationStart: () => undefined,
14
+ onStep: () => undefined,
15
+ };
16
+ /**
17
+ * Creates an Observability implementation that logs to the console using the Logger.
18
+ *
19
+ * @returns {Observability} The logger-based observability implementation.
20
+ */
21
+ export function createLoggerObservability() {
22
+ //
23
+ return {
24
+ onOperationEnd: (ctx) => {
25
+ const { durationMs, outcome, ...rest } = ctx;
26
+ const message = outcome === 'success'
27
+ ? `[storage] ${rest.operation} succeeded (${durationMs}ms)`
28
+ : `[storage] ${rest.operation} failed (${durationMs}ms)`;
29
+ if (outcome === 'success') {
30
+ Logger.info({ contextOrSpacesAfter: rest, message });
31
+ }
32
+ else {
33
+ Logger.error({ contextOrErrorOrSpacesAfter: rest, message });
34
+ }
35
+ },
36
+ onOperationStart: (ctx) => {
37
+ Logger.info({ contextOrSpacesAfter: ctx, message: `[storage] ${ctx.operation} started` });
38
+ },
39
+ onStep: (ctx) => {
40
+ Logger.info({
41
+ contextOrSpacesAfter: ctx,
42
+ message: `[storage] ${ctx.operation} ${ctx.phase} ${ctx.step}`,
43
+ });
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,51 @@
1
+ import { type OperationHooks } from '../types/hooks.js';
2
+ import { type OperationContext } from '../types/operation-context.js';
3
+ import { type Observability } from './observability.js';
4
+ export interface SagaStep {
5
+ compensate?: () => Promise<void>;
6
+ execute: () => Promise<void>;
7
+ name: string;
8
+ }
9
+ export interface RunSagaOptions<TContext extends OperationContext, TResult> {
10
+ context: TContext;
11
+ hooks?: OperationHooks<TContext, TResult>;
12
+ observability?: Observability;
13
+ result: () => Promise<TResult> | TResult;
14
+ steps: SagaStep[];
15
+ }
16
+ /**
17
+ * Runs a saga consisting of a sequence of steps, handling compensation in case of errors and
18
+ * invoking lifecycle hooks and observability callbacks.
19
+ *
20
+ * @template TContext - The type of the operation context.
21
+ * @template TResult - The type of the result returned by the saga.
22
+ * @param {RunSagaOptions<TContext, TResult>} options - Options for running the saga, including context, hooks, observability, result function, and saga steps.
23
+ * @returns {Promise<TResult>} The result of the saga execution.
24
+ *
25
+ * @throws {StorageError} If a storage step fails, or CompensationError if compensations fail.
26
+ * @throws {unknown} If `onSuccess` / a non-storage failure throws and compensations succeed, the original error is rethrown (so callers can keep HttpException status codes).
27
+ *
28
+ * @remarks
29
+ * - If a step or `onSuccess` fails, `onRollback` runs first (restore external state while storage resources still exist), then completed steps are compensated in reverse order.
30
+ * - Lifecycle hooks are invoked at the start, on success, on rollback, on error, and finally.
31
+ * - Observability callbacks are fired at various phases (operation start, step execution/compensation, operation end).
32
+ */
33
+ export declare function runSaga<TContext extends OperationContext, TResult>(options: RunSagaOptions<TContext, TResult>): Promise<TResult>;
34
+ /**
35
+ * Runs a single-step storage operation (such as read or validation) with standard lifecycle hooks and observability.
36
+ *
37
+ * @template TContext - The type of the operation context.
38
+ * @template TResult - The type of the result returned by the operation.
39
+ * @param options - Configuration for the operation.
40
+ * @param options.context - The context for the operation, passed to all lifecycle hooks and observability.
41
+ * @param options.execute - An async function that performs the main operation and returns the result.
42
+ * @param options.hooks - Lifecycle hooks for the operation (start, success, error, rollback, finally).
43
+ * @param [options.observability] - Optional observability integration for reporting operation/step lifecycle events.
44
+ * @returns A promise that resolves with the result of the operation, or rejects with a StorageError if it fails.
45
+ */
46
+ export declare function runOperation<TContext extends OperationContext, TResult>(options: {
47
+ context: TContext;
48
+ execute: () => Promise<TResult>;
49
+ hooks?: OperationHooks<TContext, TResult>;
50
+ observability?: Observability;
51
+ }): Promise<TResult>;
@@ -0,0 +1,124 @@
1
+ import { CompensationError, StorageError, toStorageError } from '@tmlmobilidade/go-clients-oci-storage';
2
+ import { noopObservability } from './observability.js';
3
+ /**
4
+ * Runs a saga consisting of a sequence of steps, handling compensation in case of errors and
5
+ * invoking lifecycle hooks and observability callbacks.
6
+ *
7
+ * @template TContext - The type of the operation context.
8
+ * @template TResult - The type of the result returned by the saga.
9
+ * @param {RunSagaOptions<TContext, TResult>} options - Options for running the saga, including context, hooks, observability, result function, and saga steps.
10
+ * @returns {Promise<TResult>} The result of the saga execution.
11
+ *
12
+ * @throws {StorageError} If a storage step fails, or CompensationError if compensations fail.
13
+ * @throws {unknown} If `onSuccess` / a non-storage failure throws and compensations succeed, the original error is rethrown (so callers can keep HttpException status codes).
14
+ *
15
+ * @remarks
16
+ * - If a step or `onSuccess` fails, `onRollback` runs first (restore external state while storage resources still exist), then completed steps are compensated in reverse order.
17
+ * - Lifecycle hooks are invoked at the start, on success, on rollback, on error, and finally.
18
+ * - Observability callbacks are fired at various phases (operation start, step execution/compensation, operation end).
19
+ */
20
+ export async function runSaga(options) {
21
+ //
22
+ const { context, result, steps } = options;
23
+ const hooks = options.hooks ?? {};
24
+ const observability = options.observability ?? noopObservability;
25
+ const startedAt = Date.now();
26
+ const completed = [];
27
+ observability.onOperationStart(context);
28
+ await hooks.onStart?.(context);
29
+ try {
30
+ //
31
+ /**
32
+ * Execute each saga step in sequence:
33
+ * - For every step, trigger an observability event before execution.
34
+ * - Await the step's execution logic.
35
+ * - Once successful, record the step as completed to enable compensation if needed.
36
+ */
37
+ for (const step of steps) {
38
+ observability.onStep({ ...context, phase: 'execute', step: step.name });
39
+ await step.execute();
40
+ completed.push(step);
41
+ }
42
+ const value = await result();
43
+ await hooks.onSuccess?.(context, value);
44
+ observability.onOperationEnd({ ...context, durationMs: Date.now() - startedAt, outcome: 'success' });
45
+ return value;
46
+ }
47
+ catch (error) {
48
+ //
49
+ const storageError = toStorageError(error, { operation: context.operation });
50
+ const suppressed = [];
51
+ /**
52
+ * Restore external state before compensating storage steps, so callers can point
53
+ * dependents back at the previous resource while it (and the new one) still exist.
54
+ */
55
+ try {
56
+ await hooks.onRollback?.(context, storageError);
57
+ }
58
+ catch (rollbackFailure) {
59
+ suppressed.push(rollbackFailure);
60
+ }
61
+ /**
62
+ * Compensate all completed steps in reverse order, if they have a compensate handler.
63
+ * For each step that defines a compensate function:
64
+ * 1. Fires an observability callback before compensation.
65
+ * 2. Awaits the compensation.
66
+ * 3. If compensation throws, collects the error for reporting as suppressed.
67
+ */
68
+ for (const step of completed.reverse()) {
69
+ if (!step.compensate)
70
+ continue;
71
+ try {
72
+ observability.onStep({ ...context, phase: 'compensate', step: step.name });
73
+ await step.compensate();
74
+ }
75
+ catch (compensationFailure) {
76
+ suppressed.push(compensationFailure);
77
+ }
78
+ }
79
+ /**
80
+ * If rollback/compensations failed, wrap as CompensationError.
81
+ * Otherwise prefer the original error when it was not already a StorageError
82
+ * (typical for HttpException thrown from onSuccess side effects).
83
+ */
84
+ const finalError = suppressed.length > 0
85
+ ? new CompensationError(storageError.message, { cause: storageError, context: { ...storageError.context, ...context }, suppressed })
86
+ : storageError;
87
+ await hooks.onError?.(context, finalError);
88
+ observability.onOperationEnd({ ...context, durationMs: Date.now() - startedAt, outcome: 'error' });
89
+ if (suppressed.length > 0)
90
+ throw finalError;
91
+ if (!(error instanceof StorageError))
92
+ throw error;
93
+ throw finalError;
94
+ }
95
+ finally {
96
+ //
97
+ await hooks.onFinally?.(context);
98
+ }
99
+ }
100
+ /**
101
+ * Runs a single-step storage operation (such as read or validation) with standard lifecycle hooks and observability.
102
+ *
103
+ * @template TContext - The type of the operation context.
104
+ * @template TResult - The type of the result returned by the operation.
105
+ * @param options - Configuration for the operation.
106
+ * @param options.context - The context for the operation, passed to all lifecycle hooks and observability.
107
+ * @param options.execute - An async function that performs the main operation and returns the result.
108
+ * @param options.hooks - Lifecycle hooks for the operation (start, success, error, rollback, finally).
109
+ * @param [options.observability] - Optional observability integration for reporting operation/step lifecycle events.
110
+ * @returns A promise that resolves with the result of the operation, or rejects with a StorageError if it fails.
111
+ */
112
+ export async function runOperation(options) {
113
+ let value;
114
+ return runSaga({
115
+ context: options.context,
116
+ hooks: options.hooks,
117
+ observability: options.observability,
118
+ result: () => value,
119
+ steps: [{
120
+ execute: async () => { value = await options.execute(); },
121
+ name: 'execute',
122
+ }],
123
+ });
124
+ }
@@ -0,0 +1,22 @@
1
+ import { type Attachment } from '@tmlmobilidade/types';
2
+ /**
3
+ * Generates a storage key for an attachment.
4
+ * @param {Pick<Attachment, '_id' | 'name' | 'resource_id' | 'scope'>} file - The attachment to generate a key for.
5
+ * @returns The generated storage key.
6
+ */
7
+ export declare function storageKey(file: Pick<Attachment, '_id' | 'name' | 'resource_id' | 'scope'>): string;
8
+ /**
9
+ * Builds a storage key for an attachment.
10
+ * @param scopeThe scope of the attachment.
11
+ * @param resourceIdThe resource ID of the attachment.
12
+ * @param idThe ID of the attachment.
13
+ * @param fileNameThe name of the attachment.
14
+ * @returns The generated storage key.
15
+ */
16
+ export declare function buildStorageKey(scope: string, resourceId: string, id: string, fileName: string): string;
17
+ /**
18
+ * Generates a temporary storage key for an attachment.
19
+ * @param key The key of the attachment.
20
+ * @returns The generated temporary storage key.
21
+ */
22
+ export declare function tempStorageKey(key: string): string;
@@ -0,0 +1,29 @@
1
+ /* * */
2
+ import { getFileExtension } from './mime.js';
3
+ /**
4
+ * Generates a storage key for an attachment.
5
+ * @param {Pick<Attachment, '_id' | 'name' | 'resource_id' | 'scope'>} file - The attachment to generate a key for.
6
+ * @returns The generated storage key.
7
+ */
8
+ export function storageKey(file) {
9
+ return `${file.scope}/${file.resource_id}/${file._id}.${getFileExtension(file.name)}`;
10
+ }
11
+ /**
12
+ * Builds a storage key for an attachment.
13
+ * @param scopeThe scope of the attachment.
14
+ * @param resourceIdThe resource ID of the attachment.
15
+ * @param idThe ID of the attachment.
16
+ * @param fileNameThe name of the attachment.
17
+ * @returns The generated storage key.
18
+ */
19
+ export function buildStorageKey(scope, resourceId, id, fileName) {
20
+ return `${scope}/${resourceId}/${id}.${getFileExtension(fileName)}`;
21
+ }
22
+ /**
23
+ * Generates a temporary storage key for an attachment.
24
+ * @param key The key of the attachment.
25
+ * @returns The generated temporary storage key.
26
+ */
27
+ export function tempStorageKey(key) {
28
+ return `.tmp/${key}.${Date.now()}`;
29
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@tmlmobilidade/go-providers-storage",
3
+ "version": "20260723.1350.16",
4
+ "author": {
5
+ "email": "iso@tmlmobilidade.pt",
6
+ "name": "TML-ISO"
7
+ },
8
+ "license": "AGPL-3.0-or-later",
9
+ "homepage": "https://go.tmlmobilidade.pt",
10
+ "bugs": {
11
+ "url": "https://github.com/tmlmobilidade/go/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/tmlmobilidade/go.git"
16
+ },
17
+ "keywords": [
18
+ "public transit",
19
+ "tml",
20
+ "transportes metropolitanos de lisboa",
21
+ "go"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "scripts": {
33
+ "build": "tsc && resolve-tspaths",
34
+ "lint": "eslint ./src/ && tsc --noEmit",
35
+ "lint:fix": "eslint ./src/ --fix",
36
+ "watch": "tsc-watch --onSuccess 'resolve-tspaths'"
37
+ },
38
+ "dependencies": {
39
+ "@tmlmobilidade/consts": "*",
40
+ "@tmlmobilidade/go-clients-mongo": "*",
41
+ "@tmlmobilidade/go-clients-oci-storage": "*",
42
+ "@tmlmobilidade/go-interfaces-godb": "*",
43
+ "@tmlmobilidade/logger": "*",
44
+ "@tmlmobilidade/strings": "*",
45
+ "@tmlmobilidade/types": "*",
46
+ "@tmlmobilidade/utils": "*"
47
+ },
48
+ "devDependencies": {
49
+ "@tmlmobilidade/tsconfig": "*",
50
+ "@types/node": "26.1.1",
51
+ "resolve-tspaths": "0.8.23",
52
+ "tsc-watch": "7.2.1",
53
+ "tsx": "4.23.1",
54
+ "typescript": "6.0.3"
55
+ }
56
+ }