@tstdl/base 0.93.116 → 0.93.118
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/api/server/gateway.js +2 -2
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/internal.d.ts +1 -0
- package/internal.js +1 -0
- package/notification/api/notification.api.d.ts +27 -12
- package/notification/api/notification.api.js +10 -3
- package/notification/client/index.d.ts +1 -0
- package/notification/client/index.js +1 -0
- package/notification/client/notification-client.d.ts +20 -0
- package/notification/client/notification-client.js +69 -0
- package/notification/index.d.ts +2 -0
- package/notification/index.js +2 -0
- package/notification/server/api/notification.api-controller.d.ts +3 -0
- package/notification/server/api/notification.api-controller.js +5 -0
- package/notification/server/providers/in-app-channel-provider.js +4 -1
- package/notification/server/services/notification-sse.service.d.ts +5 -3
- package/notification/server/services/notification-sse.service.js +19 -7
- package/notification/server/services/notification-type.service.d.ts +1 -0
- package/notification/server/services/notification-type.service.js +5 -0
- package/notification/server/services/notification.service.d.ts +2 -0
- package/notification/server/services/notification.service.js +30 -5
- package/notification/tests/notification-api.test.js +8 -0
- package/notification/tests/notification-flow.test.js +28 -0
- package/notification/tests/notification-sse.service.test.js +10 -1
- package/notification/tests/unit/notification-client.test.d.ts +1 -0
- package/notification/tests/unit/notification-client.test.js +112 -0
- package/notification/types.d.ts +9 -0
- package/notification/types.js +6 -0
- package/object-storage/object-storage.d.ts +10 -0
- package/object-storage/s3/s3.object-storage-provider.d.ts +11 -4
- package/object-storage/s3/s3.object-storage-provider.js +29 -26
- package/object-storage/s3/s3.object-storage.d.ts +7 -4
- package/object-storage/s3/s3.object-storage.js +141 -60
- package/object-storage/s3/s3.object.d.ts +6 -0
- package/object-storage/s3/s3.object.js +1 -1
- package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +1 -0
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +334 -0
- package/package.json +4 -3
- package/rpc/adapters/readable-stream.adapter.js +27 -22
- package/rpc/endpoints/message-port.rpc-endpoint.d.ts +4 -0
- package/rpc/endpoints/message-port.rpc-endpoint.js +4 -0
- package/rpc/model.d.ts +11 -1
- package/rpc/rpc.d.ts +17 -1
- package/rpc/rpc.endpoint.js +4 -3
- package/rpc/rpc.error.d.ts +5 -1
- package/rpc/rpc.error.js +16 -3
- package/rpc/rpc.js +89 -15
- package/rpc/tests/rpc.integration.test.d.ts +1 -0
- package/rpc/tests/rpc.integration.test.js +619 -0
- package/unit-test/integration-setup.d.ts +1 -0
- package/unit-test/integration-setup.js +12 -0
- package/utils/try-ignore.d.ts +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { AuthenticationClientService } from '../../../authentication/client/authentication.service.js';
|
|
3
|
+
import { Injector, runInInjectionContext } from '../../../injector/index.js';
|
|
4
|
+
import { NotificationApiClient } from '../../../notification/api/index.js';
|
|
5
|
+
import { NotificationClient } from '../../../notification/client/notification-client.js';
|
|
6
|
+
import { configureDefaultSignalsImplementation } from '../../../signals/implementation/configure.js';
|
|
7
|
+
import { BehaviorSubject, of, Subject } from 'rxjs';
|
|
8
|
+
describe('NotificationClient', () => {
|
|
9
|
+
let injector;
|
|
10
|
+
let authenticationServiceMock;
|
|
11
|
+
let notificationApiClientMock;
|
|
12
|
+
let notificationClient;
|
|
13
|
+
const sessionId$ = new BehaviorSubject(undefined);
|
|
14
|
+
const stream$ = new Subject();
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
configureDefaultSignalsImplementation();
|
|
17
|
+
injector = new Injector('TestInjector');
|
|
18
|
+
authenticationServiceMock = {
|
|
19
|
+
sessionId$,
|
|
20
|
+
};
|
|
21
|
+
notificationApiClientMock = {
|
|
22
|
+
listInApp: vi.fn().mockResolvedValue([]),
|
|
23
|
+
unreadCount: vi.fn().mockResolvedValue(0),
|
|
24
|
+
types: vi.fn().mockResolvedValue({}),
|
|
25
|
+
stream: vi.fn().mockReturnValue(of(stream$)),
|
|
26
|
+
};
|
|
27
|
+
injector.register(AuthenticationClientService, { useValue: authenticationServiceMock });
|
|
28
|
+
injector.register(NotificationApiClient, { useValue: notificationApiClientMock });
|
|
29
|
+
notificationClient = injector.resolve(NotificationClient);
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
sessionId$.next(undefined);
|
|
34
|
+
});
|
|
35
|
+
test('should initialize with empty state', () => {
|
|
36
|
+
runInInjectionContext(injector, () => {
|
|
37
|
+
expect(notificationClient.notifications()).toEqual([]);
|
|
38
|
+
expect(notificationClient.unreadCount()).toBe(0);
|
|
39
|
+
expect(notificationClient.types()).toEqual({});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
test('should load notifications on session start', async () => {
|
|
43
|
+
const notifications = [{ id: '1', type: 'test' }];
|
|
44
|
+
const unreadCount = 5;
|
|
45
|
+
const types = { test: 'Test' };
|
|
46
|
+
notificationApiClientMock.listInApp.mockResolvedValue(notifications);
|
|
47
|
+
notificationApiClientMock.unreadCount.mockResolvedValue(unreadCount);
|
|
48
|
+
notificationApiClientMock.types.mockResolvedValue(types);
|
|
49
|
+
await runInInjectionContext(injector, async () => {
|
|
50
|
+
sessionId$.next('session-1');
|
|
51
|
+
// Wait for async operations (microtasks)
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
53
|
+
expect(notificationClient.notifications()).toEqual(notifications);
|
|
54
|
+
expect(notificationClient.unreadCount()).toBe(unreadCount);
|
|
55
|
+
expect(notificationClient.types()).toEqual(types);
|
|
56
|
+
expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 20 });
|
|
57
|
+
expect(notificationApiClientMock.unreadCount).toHaveBeenCalled();
|
|
58
|
+
expect(notificationApiClientMock.types).toHaveBeenCalled();
|
|
59
|
+
expect(notificationApiClientMock.stream).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
test('should clear notifications on session end', async () => {
|
|
63
|
+
const notifications = [{ id: '1', type: 'test' }];
|
|
64
|
+
notificationApiClientMock.listInApp.mockResolvedValue(notifications);
|
|
65
|
+
await runInInjectionContext(injector, async () => {
|
|
66
|
+
sessionId$.next('session-1');
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
68
|
+
expect(notificationClient.notifications()).toHaveLength(1);
|
|
69
|
+
sessionId$.next(undefined);
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
71
|
+
expect(notificationClient.notifications()).toEqual([]);
|
|
72
|
+
expect(notificationClient.unreadCount()).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
test('should handle new notification from stream', async () => {
|
|
76
|
+
const initialNotifications = [{ id: '1', type: 'test' }];
|
|
77
|
+
notificationApiClientMock.listInApp.mockResolvedValue(initialNotifications);
|
|
78
|
+
await runInInjectionContext(injector, async () => {
|
|
79
|
+
sessionId$.next('session-1');
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
81
|
+
const newNotification = { id: '2', type: 'test' };
|
|
82
|
+
stream$.next({ notification: newNotification, unreadCount: 1 });
|
|
83
|
+
expect(notificationClient.notifications()).toEqual([newNotification, ...initialNotifications]);
|
|
84
|
+
expect(notificationClient.unreadCount()).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
test('should handle unread count update from stream', async () => {
|
|
88
|
+
notificationApiClientMock.listInApp.mockResolvedValue([]);
|
|
89
|
+
await runInInjectionContext(injector, async () => {
|
|
90
|
+
sessionId$.next('session-1');
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
92
|
+
stream$.next({ unreadCount: 10 });
|
|
93
|
+
expect(notificationClient.unreadCount()).toBe(10);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
test('should load next page of notifications', async () => {
|
|
97
|
+
const page1 = [{ id: '2', type: 'test' }];
|
|
98
|
+
const page2 = [{ id: '1', type: 'test' }];
|
|
99
|
+
notificationApiClientMock.listInApp
|
|
100
|
+
.mockResolvedValueOnce(page1) // Initial load
|
|
101
|
+
.mockResolvedValueOnce(page2); // Pagination
|
|
102
|
+
await runInInjectionContext(injector, async () => {
|
|
103
|
+
sessionId$.next('session-1');
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
105
|
+
expect(notificationClient.notifications()).toEqual(page1);
|
|
106
|
+
notificationClient.loadNext(10);
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
108
|
+
expect(notificationClient.notifications()).toEqual([...page1, ...page2]);
|
|
109
|
+
expect(notificationApiClientMock.listInApp).toHaveBeenCalledWith({ limit: 10, after: '2' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InAppNotificationView, type NotificationDefinitionMap } from './models/index.js';
|
|
2
|
+
export declare const notificationStreamItemSchema: import("../schema/index.js").ObjectSchema<{
|
|
3
|
+
unreadCount: number;
|
|
4
|
+
notification?: InAppNotificationView<Record<string, import("./models/notification-log.model.js").NotificationDefinition<import("../types/types.js").ObjectLiteral, import("../types/types.js").ObjectLiteral>>> | undefined;
|
|
5
|
+
}>;
|
|
6
|
+
export type NotificationStreamItem<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
7
|
+
notification?: InAppNotificationView<Definitions>;
|
|
8
|
+
unreadCount: number;
|
|
9
|
+
};
|
|
@@ -3,17 +3,27 @@ import type { ObjectMetadata, ObjectStorageObject } from './object.js';
|
|
|
3
3
|
export type UploadObjectOptions = {
|
|
4
4
|
contentLength?: number;
|
|
5
5
|
contentType?: string;
|
|
6
|
+
contentDisposition?: string;
|
|
7
|
+
cacheControl?: string;
|
|
6
8
|
metadata?: ObjectMetadata;
|
|
7
9
|
};
|
|
8
10
|
export type UploadUrlOptions = {
|
|
9
11
|
contentLength?: number;
|
|
10
12
|
contentType?: string;
|
|
13
|
+
contentDisposition?: string;
|
|
14
|
+
cacheControl?: string;
|
|
11
15
|
metadata?: ObjectMetadata;
|
|
12
16
|
};
|
|
13
17
|
export type CopyObjectOptions = {
|
|
18
|
+
contentType?: string;
|
|
19
|
+
contentDisposition?: string;
|
|
20
|
+
cacheControl?: string;
|
|
14
21
|
metadata?: ObjectMetadata;
|
|
15
22
|
};
|
|
16
23
|
export type MoveObjectOptions = {
|
|
24
|
+
contentType?: string;
|
|
25
|
+
contentDisposition?: string;
|
|
26
|
+
cacheControl?: string;
|
|
17
27
|
metadata?: ObjectMetadata;
|
|
18
28
|
};
|
|
19
29
|
export type ObjectStorageConfiguration = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Injector } from '../../injector/injector.js';
|
|
1
2
|
import { ObjectStorageProvider } from '../../object-storage/index.js';
|
|
2
3
|
import { S3ObjectStorage } from './s3.object-storage.js';
|
|
3
4
|
export declare class S3ObjectStorageProviderConfig {
|
|
@@ -29,11 +30,15 @@ export declare class S3ObjectStorageProviderConfig {
|
|
|
29
30
|
* S3 secret key
|
|
30
31
|
*/
|
|
31
32
|
secretKey: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to use path-style addressing (e.g., http://s3.endpoint.tld/BUCKET/KEY) instead of virtual-host addressing (e.g., http://BUCKET.s3.endpoint.tld/KEY).
|
|
35
|
+
* Useful for local development with alternative s3 implementations.
|
|
36
|
+
*/
|
|
37
|
+
forcePathStyle?: boolean;
|
|
32
38
|
}
|
|
33
39
|
export declare class S3ObjectStorageProvider extends ObjectStorageProvider<S3ObjectStorage> {
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
constructor(config: S3ObjectStorageProviderConfig);
|
|
40
|
+
#private;
|
|
41
|
+
constructor();
|
|
37
42
|
get(module: string): S3ObjectStorage;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -41,4 +46,6 @@ export declare class S3ObjectStorageProvider extends ObjectStorageProvider<S3Obj
|
|
|
41
46
|
* @param config s3 config
|
|
42
47
|
* @param register whether to register for {@link ObjectStorage} and {@link ObjectStorageProvider}
|
|
43
48
|
*/
|
|
44
|
-
export declare function configureS3ObjectStorage(config: S3ObjectStorageProviderConfig
|
|
49
|
+
export declare function configureS3ObjectStorage(config: S3ObjectStorageProviderConfig & {
|
|
50
|
+
injector?: Injector;
|
|
51
|
+
}): void;
|
|
@@ -7,8 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import {
|
|
11
|
-
import { Singleton } from '../../injector/
|
|
10
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
11
|
+
import { inject, Singleton } from '../../injector/index.js';
|
|
12
12
|
import { Injector } from '../../injector/injector.js';
|
|
13
13
|
import { ObjectStorage, ObjectStorageProvider } from '../../object-storage/index.js';
|
|
14
14
|
import { assertDefinedPass, assertStringPass, isDefined } from '../../utils/type-guards.js';
|
|
@@ -42,35 +42,39 @@ export class S3ObjectStorageProviderConfig {
|
|
|
42
42
|
* S3 secret key
|
|
43
43
|
*/
|
|
44
44
|
secretKey;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to use path-style addressing (e.g., http://s3.endpoint.tld/BUCKET/KEY) instead of virtual-host addressing (e.g., http://BUCKET.s3.endpoint.tld/KEY).
|
|
47
|
+
* Useful for local development with alternative s3 implementations.
|
|
48
|
+
*/
|
|
49
|
+
forcePathStyle;
|
|
45
50
|
}
|
|
46
51
|
let S3ObjectStorageProvider = class S3ObjectStorageProvider extends ObjectStorageProvider {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
#config = inject(S3ObjectStorageProviderConfig);
|
|
53
|
+
#client = new S3Client({
|
|
54
|
+
endpoint: this.#config.endpoint,
|
|
55
|
+
region: this.#config.region ?? 'garage',
|
|
56
|
+
credentials: {
|
|
57
|
+
accessKeyId: this.#config.accessKey,
|
|
58
|
+
secretAccessKey: this.#config.secretKey,
|
|
59
|
+
},
|
|
60
|
+
forcePathStyle: this.#config.forcePathStyle,
|
|
61
|
+
});
|
|
62
|
+
#bucket = assertDefinedPass((this.#config.bucketPerModule == true) ? true : this.#config.bucket, 'either bucket or bucketPerModule must be specified');
|
|
63
|
+
constructor() {
|
|
50
64
|
super();
|
|
51
|
-
|
|
52
|
-
if (isDefined(config.bucket) && (config.bucketPerModule == true)) {
|
|
65
|
+
if (isDefined(this.#config.bucket) && (this.#config.bucketPerModule == true)) {
|
|
53
66
|
throw new Error('bucket and bucketPerModule is mutually exclusive');
|
|
54
67
|
}
|
|
55
|
-
this.client = new Client({
|
|
56
|
-
endPoint: hostname,
|
|
57
|
-
region: config.region,
|
|
58
|
-
port: (port.length > 0) ? parseInt(port, 10) : undefined,
|
|
59
|
-
useSSL: protocol == 'https:',
|
|
60
|
-
accessKey: config.accessKey,
|
|
61
|
-
secretKey: config.secretKey,
|
|
62
|
-
});
|
|
63
|
-
this.bucket = assertDefinedPass((config.bucketPerModule == true) ? true : config.bucket, 'either bucket or bucketPerModule must be specified');
|
|
64
68
|
}
|
|
65
69
|
get(module) {
|
|
66
|
-
const bucket = (this
|
|
67
|
-
const prefix = (this
|
|
68
|
-
return new S3ObjectStorage(this
|
|
70
|
+
const bucket = (this.#bucket == true) ? module : assertStringPass(this.#bucket);
|
|
71
|
+
const prefix = (this.#bucket == true) ? '' : ((module == '') ? '' : `${module}/`);
|
|
72
|
+
return new S3ObjectStorage(this.#client, bucket, module, prefix);
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
S3ObjectStorageProvider = __decorate([
|
|
72
76
|
Singleton(),
|
|
73
|
-
__metadata("design:paramtypes", [
|
|
77
|
+
__metadata("design:paramtypes", [])
|
|
74
78
|
], S3ObjectStorageProvider);
|
|
75
79
|
export { S3ObjectStorageProvider };
|
|
76
80
|
/**
|
|
@@ -78,10 +82,9 @@ export { S3ObjectStorageProvider };
|
|
|
78
82
|
* @param config s3 config
|
|
79
83
|
* @param register whether to register for {@link ObjectStorage} and {@link ObjectStorageProvider}
|
|
80
84
|
*/
|
|
81
|
-
export function configureS3ObjectStorage(config
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
85
|
+
export function configureS3ObjectStorage(config) {
|
|
86
|
+
const targetInjector = config.injector ?? Injector;
|
|
87
|
+
targetInjector.register(S3ObjectStorageProviderConfig, { useValue: config });
|
|
88
|
+
targetInjector.registerSingleton(ObjectStorageProvider, { useToken: S3ObjectStorageProvider });
|
|
89
|
+
targetInjector.registerSingleton(ObjectStorage, { useToken: S3ObjectStorage });
|
|
87
90
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
2
|
import { ObjectStorage, type CopyObjectOptions, type MoveObjectOptions, type ObjectStorageConfiguration, type UploadObjectOptions, type UploadUrlOptions } from '../../object-storage/index.js';
|
|
3
|
-
import { S3Object } from './s3.object.js';
|
|
3
|
+
import { S3Object, type S3BucketItemStat } from './s3.object.js';
|
|
4
4
|
export declare class S3ObjectStorage extends ObjectStorage {
|
|
5
5
|
private readonly client;
|
|
6
6
|
private readonly bucket;
|
|
7
7
|
private readonly prefix;
|
|
8
|
-
constructor(client:
|
|
8
|
+
constructor(client: S3Client, bucket: string, module: string, keyPrefix: string);
|
|
9
9
|
ensureBucketExists(region?: string, options?: {
|
|
10
10
|
objectLocking?: boolean;
|
|
11
11
|
}): Promise<void>;
|
|
12
12
|
configureBucket(configuration: ObjectStorageConfiguration): Promise<void>;
|
|
13
13
|
exists(key: string): Promise<boolean>;
|
|
14
|
-
statObject(key: string): Promise<
|
|
14
|
+
statObject(key: string): Promise<S3BucketItemStat>;
|
|
15
15
|
uploadObject(key: string, content: Uint8Array | ReadableStream<Uint8Array>, options?: UploadObjectOptions): Promise<S3Object>;
|
|
16
16
|
copyObject(source: S3Object | string, destination: S3Object | string | [ObjectStorage, string], options?: CopyObjectOptions): Promise<S3Object>;
|
|
17
17
|
moveObject(source: S3Object | string, destination: S3Object | string | [ObjectStorage, string], options?: MoveObjectOptions): Promise<S3Object>;
|
|
@@ -28,4 +28,7 @@ export declare class S3ObjectStorage extends ObjectStorage {
|
|
|
28
28
|
private getResourceUriSync;
|
|
29
29
|
private getBucketKey;
|
|
30
30
|
private getKey;
|
|
31
|
+
isNotFoundError(error: unknown): boolean;
|
|
32
|
+
isForbiddenError(error: unknown): boolean;
|
|
33
|
+
isError(error: unknown, ...names: string[]): boolean;
|
|
31
34
|
}
|
|
@@ -9,20 +9,19 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
var S3ObjectStorage_1;
|
|
11
11
|
import { Readable } from 'node:stream';
|
|
12
|
-
import {
|
|
12
|
+
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketLifecycleCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLifecycleConfigurationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutBucketLifecycleConfigurationCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
13
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
13
14
|
import { match, P } from 'ts-pattern';
|
|
14
15
|
import { Singleton } from '../../injector/decorators.js';
|
|
15
16
|
import { registerAfterResolve } from '../../injector/resolution.js';
|
|
16
17
|
import { ObjectStorage } from '../../object-storage/index.js';
|
|
17
|
-
import { toArray } from '../../utils/array/array.js';
|
|
18
|
-
import { mapAsync } from '../../utils/async-iterable-helpers/map.js';
|
|
19
18
|
import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
|
|
20
19
|
import { now } from '../../utils/date-time.js';
|
|
21
20
|
import { mapObjectKeys } from '../../utils/object/object.js';
|
|
22
21
|
import { readableStreamFromPromise } from '../../utils/stream/index.js';
|
|
23
22
|
import { readBinaryStream } from '../../utils/stream/stream-reader.js';
|
|
24
23
|
import { _throw } from '../../utils/throw.js';
|
|
25
|
-
import { assertDefinedPass, isDefined, isObject, isString, isUint8Array, isUndefined } from '../../utils/type-guards.js';
|
|
24
|
+
import { assertDefinedPass, isBlob, isDate, isDefined, isObject, isReadableStream, isString, isUint8Array, isUndefined } from '../../utils/type-guards.js';
|
|
26
25
|
import { secondsPerDay } from '../../utils/units.js';
|
|
27
26
|
import { S3ObjectStorageProvider } from './s3.object-storage-provider.js';
|
|
28
27
|
import { S3Object } from './s3.object.js';
|
|
@@ -42,27 +41,37 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
43
|
async ensureBucketExists(region, options) {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
try {
|
|
45
|
+
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
|
-
|
|
48
|
+
catch (error) {
|
|
49
|
+
// ignore error if bucket already exists (e.g. 403 Forbidden when we have no head permission but it might still exist)
|
|
50
|
+
if (this.isNotFoundError(error)) {
|
|
51
|
+
await this.client.send(new CreateBucketCommand({
|
|
52
|
+
Bucket: this.bucket,
|
|
53
|
+
CreateBucketConfiguration: isDefined(region) ? { LocationConstraint: region } : undefined,
|
|
54
|
+
ObjectLockEnabledForBucket: options?.objectLocking,
|
|
55
|
+
}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
async configureBucket(configuration) {
|
|
52
62
|
if (isUndefined(configuration.lifecycle)) {
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
|
-
let
|
|
65
|
+
let currentLifecycleRules;
|
|
56
66
|
try {
|
|
57
|
-
|
|
67
|
+
const result = await this.client.send(new GetBucketLifecycleConfigurationCommand({ Bucket: this.bucket }));
|
|
68
|
+
currentLifecycleRules = result.Rules;
|
|
58
69
|
}
|
|
59
70
|
catch (error) {
|
|
60
|
-
|
|
61
|
-
if (!isObject(error) || (error.code != 'NoSuchLifecycleConfiguration')) {
|
|
71
|
+
if (!this.isError(error, 'NoSuchLifecycleConfiguration')) {
|
|
62
72
|
throw error;
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
|
-
const currentLifecycleRules = isDefined(currentLifecycle?.Rule) ? toArray(currentLifecycle.Rule) : undefined; // https://github.com/minio/minio-js/issues/1407
|
|
66
75
|
const tstdlRule = currentLifecycleRules?.find((rule) => rule.ID == 'TstdlExpireObjects');
|
|
67
76
|
const tstdlRuleExpiration = tstdlRule?.Expiration?.Days;
|
|
68
77
|
const targetExpirationDays = configuration.lifecycle?.expiration?.after;
|
|
@@ -73,35 +82,43 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
73
82
|
const nonTstdlRules = currentLifecycleRules?.filter((rule) => rule.ID != 'TstdlExpireObjects') ?? [];
|
|
74
83
|
if (isUndefined(targetExpiration)) {
|
|
75
84
|
if (nonTstdlRules.length == 0) {
|
|
76
|
-
await this.client.
|
|
85
|
+
await this.client.send(new DeleteBucketLifecycleCommand({
|
|
86
|
+
Bucket: this.bucket,
|
|
87
|
+
}));
|
|
77
88
|
}
|
|
78
89
|
else {
|
|
79
|
-
await this.client.
|
|
90
|
+
await this.client.send(new PutBucketLifecycleConfigurationCommand({
|
|
91
|
+
Bucket: this.bucket,
|
|
92
|
+
LifecycleConfiguration: { Rules: nonTstdlRules },
|
|
93
|
+
}));
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
96
|
else {
|
|
83
|
-
await this.client.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
await this.client.send(new PutBucketLifecycleConfigurationCommand({
|
|
98
|
+
Bucket: this.bucket,
|
|
99
|
+
LifecycleConfiguration: {
|
|
100
|
+
Rules: [
|
|
101
|
+
...nonTstdlRules,
|
|
102
|
+
{
|
|
103
|
+
ID: 'TstdlExpireObjects',
|
|
104
|
+
Status: 'Enabled',
|
|
105
|
+
Expiration: {
|
|
106
|
+
Days: targetExpiration,
|
|
107
|
+
},
|
|
91
108
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
async exists(key) {
|
|
98
115
|
const bucketKey = this.getBucketKey(key);
|
|
99
116
|
try {
|
|
100
|
-
await this.client.
|
|
117
|
+
await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
|
|
101
118
|
return true;
|
|
102
119
|
}
|
|
103
120
|
catch (error) {
|
|
104
|
-
if (
|
|
121
|
+
if (this.isNotFoundError(error)) {
|
|
105
122
|
return false;
|
|
106
123
|
}
|
|
107
124
|
throw error;
|
|
@@ -109,21 +126,28 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
109
126
|
}
|
|
110
127
|
async statObject(key) {
|
|
111
128
|
const bucketKey = this.getBucketKey(key);
|
|
112
|
-
|
|
129
|
+
const result = await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
|
|
130
|
+
return {
|
|
131
|
+
size: result.ContentLength ?? 0,
|
|
132
|
+
etag: result.ETag ?? '',
|
|
133
|
+
lastModified: result.LastModified?.getTime(),
|
|
134
|
+
metadata: result.Metadata ?? {},
|
|
135
|
+
};
|
|
113
136
|
}
|
|
114
137
|
async uploadObject(key, content, options) {
|
|
115
138
|
const bucketKey = this.getBucketKey(key);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
const body = isUint8Array(content) ? content : Readable.fromWeb(content);
|
|
140
|
+
const metadata = isDefined(options?.metadata) ? mapObjectKeys(options.metadata, (key) => key.toLowerCase()) : undefined;
|
|
141
|
+
await this.client.send(new PutObjectCommand({
|
|
142
|
+
Bucket: this.bucket,
|
|
143
|
+
Key: bucketKey,
|
|
144
|
+
Body: body,
|
|
145
|
+
ContentLength: options?.contentLength,
|
|
146
|
+
ContentType: options?.contentType,
|
|
147
|
+
ContentDisposition: options?.contentDisposition,
|
|
148
|
+
CacheControl: options?.cacheControl,
|
|
149
|
+
Metadata: metadata,
|
|
150
|
+
}));
|
|
127
151
|
return await this.getObject(key);
|
|
128
152
|
}
|
|
129
153
|
async copyObject(source, destination, options) {
|
|
@@ -137,11 +161,16 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
137
161
|
.with([P.instanceOf(S3ObjectStorage_1), P.string], async ([storage, key]) => await storage.getObject(key))
|
|
138
162
|
.otherwise(() => _throw(new Error('Destination must be a S3Object, string key, or [S3ObjectStorage, string] tuple.')));
|
|
139
163
|
const sourceMetadata = await sourceObject.getMetadata();
|
|
140
|
-
|
|
164
|
+
const metadata = mapObjectKeys({ ...sourceMetadata, ...options?.metadata }, (key) => key.toLowerCase());
|
|
165
|
+
await this.client.send(new CopyObjectCommand({
|
|
166
|
+
CopySource: `${this.bucket}/${sourceObject.storage.getBucketKey(sourceObject.key)}`,
|
|
141
167
|
Bucket: destinationObject.storage.bucket,
|
|
142
|
-
|
|
168
|
+
Key: destinationObject.storage.getBucketKey(destinationObject.key),
|
|
143
169
|
MetadataDirective: 'REPLACE',
|
|
144
|
-
|
|
170
|
+
ContentType: options?.contentType,
|
|
171
|
+
ContentDisposition: options?.contentDisposition,
|
|
172
|
+
CacheControl: options?.cacheControl,
|
|
173
|
+
Metadata: metadata,
|
|
145
174
|
}));
|
|
146
175
|
return destinationObject;
|
|
147
176
|
}
|
|
@@ -153,22 +182,45 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
153
182
|
}
|
|
154
183
|
async getContent(key) {
|
|
155
184
|
const bucketKey = this.getBucketKey(key);
|
|
156
|
-
const result = await this.client.
|
|
157
|
-
|
|
185
|
+
const result = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
|
|
186
|
+
if (isUint8Array(result.Body)) {
|
|
187
|
+
return result.Body;
|
|
188
|
+
}
|
|
189
|
+
return await readBinaryStream(result.Body);
|
|
158
190
|
}
|
|
159
191
|
getContentStream(key) {
|
|
160
192
|
const bucketKey = this.getBucketKey(key);
|
|
161
193
|
return readableStreamFromPromise(async () => {
|
|
162
|
-
const
|
|
163
|
-
|
|
194
|
+
const result = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
|
|
195
|
+
if (isReadableStream(result.Body)) {
|
|
196
|
+
return result.Body;
|
|
197
|
+
}
|
|
198
|
+
if (isBlob(result.Body)) {
|
|
199
|
+
return result.Body.stream();
|
|
200
|
+
}
|
|
201
|
+
return Readable.toWeb(result.Body);
|
|
164
202
|
});
|
|
165
203
|
}
|
|
166
204
|
async getObjects() {
|
|
167
205
|
return await toArrayAsync(this.getObjectsCursor());
|
|
168
206
|
}
|
|
169
|
-
getObjectsCursor() {
|
|
170
|
-
|
|
171
|
-
|
|
207
|
+
async *getObjectsCursor() {
|
|
208
|
+
let continuationToken;
|
|
209
|
+
do {
|
|
210
|
+
const result = await this.client.send(new ListObjectsV2Command({
|
|
211
|
+
Bucket: this.bucket,
|
|
212
|
+
Prefix: this.prefix,
|
|
213
|
+
ContinuationToken: continuationToken,
|
|
214
|
+
}));
|
|
215
|
+
if (isDefined(result.Contents)) {
|
|
216
|
+
for (const item of result.Contents) {
|
|
217
|
+
if (isDefined(item.Key)) {
|
|
218
|
+
yield new S3Object(this.module, this.getKey(item.Key), `s3://${this.bucket}/${item.Key}`, item.Size, this);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
continuationToken = result.NextContinuationToken;
|
|
223
|
+
} while (isDefined(continuationToken));
|
|
172
224
|
}
|
|
173
225
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
174
226
|
async getObject(key) {
|
|
@@ -180,22 +232,42 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
180
232
|
}
|
|
181
233
|
async getDownloadUrl(key, expirationTimestamp, responseHeaders) {
|
|
182
234
|
const bucketKey = this.getBucketKey(key);
|
|
183
|
-
const
|
|
184
|
-
|
|
235
|
+
const expiration = getExpiration(expirationTimestamp);
|
|
236
|
+
const expiresHeader = responseHeaders?.['Expires'];
|
|
237
|
+
const mappedExpiresHeader = isDefined(expiresHeader) ? (isDate(expiresHeader) ? expiresHeader : new Date(expiresHeader)) : undefined;
|
|
238
|
+
return await getSignedUrl(this.client, new GetObjectCommand({
|
|
239
|
+
Bucket: this.bucket,
|
|
240
|
+
Key: bucketKey,
|
|
241
|
+
ResponseContentType: responseHeaders?.['Content-Type'],
|
|
242
|
+
ResponseContentDisposition: responseHeaders?.['Content-Disposition'],
|
|
243
|
+
ResponseCacheControl: responseHeaders?.['Cache-Control'],
|
|
244
|
+
ResponseContentLanguage: responseHeaders?.['Content-Language'],
|
|
245
|
+
ResponseContentEncoding: responseHeaders?.['Content-Encoding'],
|
|
246
|
+
ResponseExpires: mappedExpiresHeader,
|
|
247
|
+
}), { expiresIn: expiration });
|
|
185
248
|
}
|
|
186
249
|
async getUploadUrl(key, expirationTimestamp, options) {
|
|
187
250
|
const bucketKey = this.getBucketKey(key);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
const expiration = getExpiration(expirationTimestamp);
|
|
252
|
+
return await getSignedUrl(this.client, new PutObjectCommand({
|
|
253
|
+
Bucket: this.bucket,
|
|
254
|
+
Key: bucketKey,
|
|
255
|
+
Metadata: options?.metadata,
|
|
256
|
+
ContentType: options?.contentType,
|
|
257
|
+
ContentDisposition: options?.contentDisposition,
|
|
258
|
+
CacheControl: options?.cacheControl,
|
|
259
|
+
}), { expiresIn: expiration });
|
|
191
260
|
}
|
|
192
261
|
async deleteObject(key) {
|
|
193
262
|
const bucketKey = this.getBucketKey(key);
|
|
194
|
-
await this.client.
|
|
263
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
|
|
195
264
|
}
|
|
196
265
|
async deleteObjects(keys) {
|
|
197
|
-
const bucketKeys = keys.map((key) => this.getBucketKey(key));
|
|
198
|
-
await this.client.
|
|
266
|
+
const bucketKeys = keys.map((key) => ({ Key: this.getBucketKey(key) }));
|
|
267
|
+
await this.client.send(new DeleteObjectsCommand({
|
|
268
|
+
Bucket: this.bucket,
|
|
269
|
+
Delete: { Objects: bucketKeys },
|
|
270
|
+
}));
|
|
199
271
|
}
|
|
200
272
|
getResourceUriSync(key) {
|
|
201
273
|
const bucketKey = this.getBucketKey(key);
|
|
@@ -207,6 +279,15 @@ let S3ObjectStorage = S3ObjectStorage_1 = class S3ObjectStorage extends ObjectSt
|
|
|
207
279
|
getKey(bucketKey) {
|
|
208
280
|
return bucketKey.slice(this.prefix.length);
|
|
209
281
|
}
|
|
282
|
+
isNotFoundError(error) {
|
|
283
|
+
return this.isError(error, 'NotFound', 'NoSuchKey');
|
|
284
|
+
}
|
|
285
|
+
isForbiddenError(error) {
|
|
286
|
+
return this.isError(error, 'Forbidden', 'AccessDenied', 'InvalidAccessKeyId', 'SignatureDoesNotMatch') || ((isObject(error) && isObject(error.$metadata) && error.$metadata.httpStatusCode == 403));
|
|
287
|
+
}
|
|
288
|
+
isError(error, ...names) {
|
|
289
|
+
return isObject(error) && names.includes(error.name);
|
|
290
|
+
}
|
|
210
291
|
};
|
|
211
292
|
S3ObjectStorage = S3ObjectStorage_1 = __decorate([
|
|
212
293
|
Singleton({
|
|
@@ -218,11 +299,11 @@ S3ObjectStorage = S3ObjectStorage_1 = __decorate([
|
|
|
218
299
|
},
|
|
219
300
|
argumentIdentityProvider: JSON.stringify,
|
|
220
301
|
}),
|
|
221
|
-
__metadata("design:paramtypes", [
|
|
302
|
+
__metadata("design:paramtypes", [S3Client, String, String, String])
|
|
222
303
|
], S3ObjectStorage);
|
|
223
304
|
export { S3ObjectStorage };
|
|
224
|
-
function
|
|
305
|
+
function getExpiration(expirationTimestamp) {
|
|
225
306
|
const date = now();
|
|
226
307
|
const diffSeconds = Math.floor((expirationTimestamp - date.getTime()) / 1000);
|
|
227
|
-
return
|
|
308
|
+
return diffSeconds;
|
|
228
309
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ObjectMetadata } from '../../object-storage/object.js';
|
|
2
2
|
import { ObjectStorageObject } from '../../object-storage/object.js';
|
|
3
3
|
import type { S3ObjectStorage } from './s3.object-storage.js';
|
|
4
|
+
export type S3BucketItemStat = {
|
|
5
|
+
size: number;
|
|
6
|
+
etag?: string;
|
|
7
|
+
lastModified?: number;
|
|
8
|
+
metadata: ObjectMetadata;
|
|
9
|
+
};
|
|
4
10
|
export declare class S3Object extends ObjectStorageObject {
|
|
5
11
|
private readonly resourceUri;
|
|
6
12
|
private contentLength;
|