@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.
Files changed (53) hide show
  1. package/api/server/gateway.js +2 -2
  2. package/index.d.ts +1 -0
  3. package/index.js +1 -0
  4. package/internal.d.ts +1 -0
  5. package/internal.js +1 -0
  6. package/notification/api/notification.api.d.ts +27 -12
  7. package/notification/api/notification.api.js +10 -3
  8. package/notification/client/index.d.ts +1 -0
  9. package/notification/client/index.js +1 -0
  10. package/notification/client/notification-client.d.ts +20 -0
  11. package/notification/client/notification-client.js +69 -0
  12. package/notification/index.d.ts +2 -0
  13. package/notification/index.js +2 -0
  14. package/notification/server/api/notification.api-controller.d.ts +3 -0
  15. package/notification/server/api/notification.api-controller.js +5 -0
  16. package/notification/server/providers/in-app-channel-provider.js +4 -1
  17. package/notification/server/services/notification-sse.service.d.ts +5 -3
  18. package/notification/server/services/notification-sse.service.js +19 -7
  19. package/notification/server/services/notification-type.service.d.ts +1 -0
  20. package/notification/server/services/notification-type.service.js +5 -0
  21. package/notification/server/services/notification.service.d.ts +2 -0
  22. package/notification/server/services/notification.service.js +30 -5
  23. package/notification/tests/notification-api.test.js +8 -0
  24. package/notification/tests/notification-flow.test.js +28 -0
  25. package/notification/tests/notification-sse.service.test.js +10 -1
  26. package/notification/tests/unit/notification-client.test.d.ts +1 -0
  27. package/notification/tests/unit/notification-client.test.js +112 -0
  28. package/notification/types.d.ts +9 -0
  29. package/notification/types.js +6 -0
  30. package/object-storage/object-storage.d.ts +10 -0
  31. package/object-storage/s3/s3.object-storage-provider.d.ts +11 -4
  32. package/object-storage/s3/s3.object-storage-provider.js +29 -26
  33. package/object-storage/s3/s3.object-storage.d.ts +7 -4
  34. package/object-storage/s3/s3.object-storage.js +141 -60
  35. package/object-storage/s3/s3.object.d.ts +6 -0
  36. package/object-storage/s3/s3.object.js +1 -1
  37. package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +1 -0
  38. package/object-storage/s3/tests/s3.object-storage.integration.test.js +334 -0
  39. package/package.json +4 -3
  40. package/rpc/adapters/readable-stream.adapter.js +27 -22
  41. package/rpc/endpoints/message-port.rpc-endpoint.d.ts +4 -0
  42. package/rpc/endpoints/message-port.rpc-endpoint.js +4 -0
  43. package/rpc/model.d.ts +11 -1
  44. package/rpc/rpc.d.ts +17 -1
  45. package/rpc/rpc.endpoint.js +4 -3
  46. package/rpc/rpc.error.d.ts +5 -1
  47. package/rpc/rpc.error.js +16 -3
  48. package/rpc/rpc.js +89 -15
  49. package/rpc/tests/rpc.integration.test.d.ts +1 -0
  50. package/rpc/tests/rpc.integration.test.js +619 -0
  51. package/unit-test/integration-setup.d.ts +1 -0
  52. package/unit-test/integration-setup.js +12 -0
  53. package/utils/try-ignore.d.ts +2 -2
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ import { number, object, optional } from '../schema/index.js';
2
+ import { InAppNotificationView } from './models/index.js';
3
+ export const notificationStreamItemSchema = object({
4
+ notification: optional(InAppNotificationView),
5
+ unreadCount: number(),
6
+ });
@@ -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 readonly client;
35
- private readonly bucket;
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, register?: boolean): void;
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 { Client } from 'minio';
11
- import { Singleton } from '../../injector/decorators.js';
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
- client;
48
- bucket;
49
- constructor(config) {
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
- const { hostname, port, protocol } = new URL(config.endpoint);
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.bucket == true) ? module : assertStringPass(this.bucket);
67
- const prefix = (this.bucket == true) ? '' : ((module == '') ? '' : `${module}/`);
68
- return new S3ObjectStorage(this.client, bucket, module, prefix);
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", [S3ObjectStorageProviderConfig])
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, register = true) {
82
- Injector.register(S3ObjectStorageProviderConfig, { useValue: config });
83
- if (register) {
84
- Injector.registerSingleton(ObjectStorageProvider, { useToken: S3ObjectStorageProvider });
85
- Injector.registerSingleton(ObjectStorage, { useToken: S3ObjectStorage });
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 { type BucketItemStat, type Client } from 'minio';
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: Client, bucket: string, module: string, keyPrefix: string);
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<BucketItemStat>;
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 { CopyDestinationOptions, CopySourceOptions } from 'minio';
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
- const exists = await this.client.bucketExists(this.bucket);
46
- if (exists) {
44
+ try {
45
+ await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
47
46
  return;
48
47
  }
49
- await this.client.makeBucket(this.bucket, region ?? '', { ObjectLocking: options?.objectLocking ?? false });
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 currentLifecycle = null;
65
+ let currentLifecycleRules;
56
66
  try {
57
- currentLifecycle = await this.client.getBucketLifecycle(this.bucket);
67
+ const result = await this.client.send(new GetBucketLifecycleConfigurationCommand({ Bucket: this.bucket }));
68
+ currentLifecycleRules = result.Rules;
58
69
  }
59
70
  catch (error) {
60
- // ignore error if lifecycle configuration is not set
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.removeBucketLifecycle(this.bucket);
85
+ await this.client.send(new DeleteBucketLifecycleCommand({
86
+ Bucket: this.bucket,
87
+ }));
77
88
  }
78
89
  else {
79
- await this.client.setBucketLifecycle(this.bucket, { Rule: nonTstdlRules });
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.setBucketLifecycle(this.bucket, {
84
- Rule: [
85
- ...nonTstdlRules,
86
- {
87
- ID: 'TstdlExpireObjects',
88
- Status: 'Enabled',
89
- Expiration: {
90
- Days: targetExpiration,
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.statObject(this.bucket, bucketKey);
117
+ await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: bucketKey }));
101
118
  return true;
102
119
  }
103
120
  catch (error) {
104
- if (isObject(error) && error.code == 'NotFound') {
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
- return await this.client.statObject(this.bucket, bucketKey);
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
- if (isUint8Array(content)) {
117
- await this.client.putObject(this.bucket, bucketKey, Buffer.from(content), options?.contentLength, options?.metadata);
118
- }
119
- else {
120
- const readable = Readable.fromWeb(content);
121
- const errorPromise = new Promise((_, reject) => readable.on('error', reject));
122
- await Promise.race([
123
- this.client.putObject(this.bucket, bucketKey, readable, options?.contentLength, options?.metadata),
124
- errorPromise,
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
- await this.client.copyObject(new CopySourceOptions({ Bucket: this.bucket, Object: sourceObject.storage.getBucketKey(sourceObject.key) }), new CopyDestinationOptions({
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
- Object: destinationObject.storage.getBucketKey(destinationObject.key),
168
+ Key: destinationObject.storage.getBucketKey(destinationObject.key),
143
169
  MetadataDirective: 'REPLACE',
144
- UserMetadata: { ...sourceMetadata, ...options?.metadata },
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.getObject(this.bucket, bucketKey);
157
- return await readBinaryStream(result);
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 readable = await this.client.getObject(this.bucket, bucketKey);
163
- return Readable.toWeb(readable);
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
- const stream = this.client.listObjectsV2(this.bucket, this.prefix, true);
171
- return mapAsync(stream, (item) => new S3Object(this.module, this.getKey(item.name), `s3://${this.bucket}/${item.name}`, item.size, this));
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 { date, expiration } = getDateAndExpiration(expirationTimestamp);
184
- return await this.client.presignedGetObject(this.bucket, bucketKey, expiration, responseHeaders ?? {}, date);
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 { date, expiration } = getDateAndExpiration(expirationTimestamp);
189
- const query = mapObjectKeys(options?.metadata ?? {}, (key) => `X-Amz-Meta-${key}`);
190
- return await this.client.presignedUrl('PUT', this.bucket, bucketKey, expiration, query, date);
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.removeObject(this.bucket, bucketKey);
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.removeObjects(this.bucket, bucketKeys);
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", [Function, String, String, String])
302
+ __metadata("design:paramtypes", [S3Client, String, String, String])
222
303
  ], S3ObjectStorage);
223
304
  export { S3ObjectStorage };
224
- function getDateAndExpiration(expirationTimestamp) {
305
+ function getExpiration(expirationTimestamp) {
225
306
  const date = now();
226
307
  const diffSeconds = Math.floor((expirationTimestamp - date.getTime()) / 1000);
227
- return { date, expiration: diffSeconds };
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;