@tstdl/base 0.93.117 → 0.93.119
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 +22 -10
- package/notification/api/notification.api.js +9 -3
- package/notification/client/notification-client.d.ts +2 -0
- package/notification/client/notification-client.js +7 -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/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 +9 -2
- package/notification/tests/notification-api.test.js +8 -0
- package/notification/tests/notification-sse.service.test.js +9 -0
- package/notification/tests/unit/notification-client.test.d.ts +1 -0
- package/notification/tests/unit/notification-client.test.js +112 -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 +8 -4
- package/object-storage/s3/s3.object-storage.js +148 -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 +3 -2
- 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/api/server/gateway.js
CHANGED
|
@@ -190,7 +190,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
190
190
|
const decodedUrlParameters = mapObjectValues(context.resourcePatternResult.pathname.groups, (value) => isDefined(value) ? decodeURIComponent(value) : undefined);
|
|
191
191
|
const parameters = { ...context.request.query.asObject(), ...bodyAsParameters, ...decodedUrlParameters };
|
|
192
192
|
const validatedParameters = isDefined(context.endpoint.definition.parameters)
|
|
193
|
-
? Schema.parse(context.endpoint.definition.parameters, parameters)
|
|
193
|
+
? Schema.parse(context.endpoint.definition.parameters, parameters, { coerce: true })
|
|
194
194
|
: parameters;
|
|
195
195
|
const requestTokenProvider = this.#requestTokenProvider;
|
|
196
196
|
const auditor = this.#auditor;
|
|
@@ -279,5 +279,5 @@ async function getBody(request, options, schema) {
|
|
|
279
279
|
body = await request.body.readAsBuffer(options);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
return Schema.parse(schema, body);
|
|
282
|
+
return Schema.parse(schema, body, { coerce: true });
|
|
283
283
|
}
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/internal.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const internal: unique symbol;
|
package/internal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const internal = Symbol('internal');
|
|
@@ -14,6 +14,12 @@ export declare const notificationApiDefinition: {
|
|
|
14
14
|
};
|
|
15
15
|
credentials: true;
|
|
16
16
|
};
|
|
17
|
+
types: {
|
|
18
|
+
resource: string;
|
|
19
|
+
method: "GET";
|
|
20
|
+
result: import("../../schema/index.js").ObjectSchema<Partial<import("../../schema/index.js").Record<string, string>>>;
|
|
21
|
+
credentials: true;
|
|
22
|
+
};
|
|
17
23
|
listInApp: {
|
|
18
24
|
resource: string;
|
|
19
25
|
method: "GET";
|
|
@@ -77,9 +83,9 @@ export declare const notificationApiDefinition: {
|
|
|
77
83
|
resource: string;
|
|
78
84
|
method: "POST";
|
|
79
85
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
type: string;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
channel: "email" | "in-app" | "web-push";
|
|
83
89
|
}>;
|
|
84
90
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
85
91
|
credentials: true;
|
|
@@ -88,11 +94,11 @@ export declare const notificationApiDefinition: {
|
|
|
88
94
|
resource: string;
|
|
89
95
|
method: "POST";
|
|
90
96
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
91
|
-
|
|
92
|
-
readonly keys: {
|
|
97
|
+
keys: {
|
|
93
98
|
p256dhBase64: string;
|
|
94
99
|
authBase64: string;
|
|
95
100
|
};
|
|
101
|
+
endpoint: string;
|
|
96
102
|
}>;
|
|
97
103
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
98
104
|
credentials: true;
|
|
@@ -111,6 +117,12 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
111
117
|
};
|
|
112
118
|
credentials: true;
|
|
113
119
|
};
|
|
120
|
+
types: {
|
|
121
|
+
resource: string;
|
|
122
|
+
method: "GET";
|
|
123
|
+
result: import("../../schema/index.js").ObjectSchema<Partial<import("../../schema/index.js").Record<string, string>>>;
|
|
124
|
+
credentials: true;
|
|
125
|
+
};
|
|
114
126
|
listInApp: {
|
|
115
127
|
resource: string;
|
|
116
128
|
method: "GET";
|
|
@@ -174,9 +186,9 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
174
186
|
resource: string;
|
|
175
187
|
method: "POST";
|
|
176
188
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
type: string;
|
|
190
|
+
enabled: boolean;
|
|
191
|
+
channel: "email" | "in-app" | "web-push";
|
|
180
192
|
}>;
|
|
181
193
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
182
194
|
credentials: true;
|
|
@@ -185,11 +197,11 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
185
197
|
resource: string;
|
|
186
198
|
method: "POST";
|
|
187
199
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
188
|
-
|
|
189
|
-
readonly keys: {
|
|
200
|
+
keys: {
|
|
190
201
|
p256dhBase64: string;
|
|
191
202
|
authBase64: string;
|
|
192
203
|
};
|
|
204
|
+
endpoint: string;
|
|
193
205
|
}>;
|
|
194
206
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
195
207
|
credentials: true;
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { compileClient } from '../../api/client/index.js';
|
|
8
8
|
import { defineApi } from '../../api/types.js';
|
|
9
9
|
import { ReplaceClass } from '../../injector/decorators.js';
|
|
10
|
-
import { array, boolean, enumeration,
|
|
10
|
+
import { array, boolean, enumeration, literal, number, object, optional, record, string } from '../../schema/index.js';
|
|
11
11
|
import { DataStream } from '../../sse/data-stream.js';
|
|
12
12
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
13
13
|
import { NotificationChannel } from '../models/index.js';
|
|
@@ -20,6 +20,12 @@ export const notificationApiDefinition = defineApi({
|
|
|
20
20
|
result: (DataStream),
|
|
21
21
|
credentials: true,
|
|
22
22
|
},
|
|
23
|
+
types: {
|
|
24
|
+
resource: 'types',
|
|
25
|
+
method: 'GET',
|
|
26
|
+
result: record(string(), string()),
|
|
27
|
+
credentials: true,
|
|
28
|
+
},
|
|
23
29
|
listInApp: {
|
|
24
30
|
resource: 'in-app',
|
|
25
31
|
method: 'GET',
|
|
@@ -78,7 +84,7 @@ export const notificationApiDefinition = defineApi({
|
|
|
78
84
|
updatePreference: {
|
|
79
85
|
resource: 'preferences',
|
|
80
86
|
method: 'POST',
|
|
81
|
-
parameters:
|
|
87
|
+
parameters: object({
|
|
82
88
|
type: string(),
|
|
83
89
|
channel: enumeration(NotificationChannel),
|
|
84
90
|
enabled: boolean(),
|
|
@@ -89,7 +95,7 @@ export const notificationApiDefinition = defineApi({
|
|
|
89
95
|
registerWebPush: {
|
|
90
96
|
resource: 'web-push/register',
|
|
91
97
|
method: 'POST',
|
|
92
|
-
parameters:
|
|
98
|
+
parameters: object({
|
|
93
99
|
endpoint: string(),
|
|
94
100
|
keys: object({
|
|
95
101
|
p256dhBase64: string(),
|
|
@@ -10,9 +10,11 @@ export declare class NotificationClient<Definitions extends NotificationDefiniti
|
|
|
10
10
|
readonly state$: import("rxjs").Observable<NotificationState<Definitions>>;
|
|
11
11
|
readonly notifications$: import("rxjs").Observable<InAppNotificationView<Definitions>[]>;
|
|
12
12
|
readonly unreadCount$: import("rxjs").Observable<number>;
|
|
13
|
+
readonly types$: import("rxjs").Observable<Record<keyof Definitions, string>>;
|
|
13
14
|
readonly state: import("../../signals/api.js").Signal<NotificationState<Definitions>>;
|
|
14
15
|
readonly notifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
15
16
|
readonly unreadCount: import("../../signals/api.js").Signal<number>;
|
|
17
|
+
readonly types: import("../../signals/api.js").Signal<Record<keyof Definitions, string>>;
|
|
16
18
|
loadNext(count?: number): void;
|
|
17
19
|
}
|
|
18
20
|
export {};
|
|
@@ -47,9 +47,16 @@ let NotificationClient = class NotificationClient {
|
|
|
47
47
|
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
48
48
|
notifications$ = this.state$.pipe(map((state) => state.notifications));
|
|
49
49
|
unreadCount$ = this.state$.pipe(map((state) => state.unreadCount));
|
|
50
|
+
types$ = this.#authenticationService.sessionId$.pipe(switchMap((sessionId) => {
|
|
51
|
+
if (isUndefined(sessionId)) {
|
|
52
|
+
return of({});
|
|
53
|
+
}
|
|
54
|
+
return this.api.types();
|
|
55
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
50
56
|
state = toSignal(this.state$, { initialValue: { notifications: [], unreadCount: 0 } });
|
|
51
57
|
notifications = computed(() => this.state().notifications);
|
|
52
58
|
unreadCount = computed(() => this.state().unreadCount);
|
|
59
|
+
types = toSignal(this.types$, { initialValue: {} });
|
|
53
60
|
loadNext(count = 20) {
|
|
54
61
|
const current = this.notifications();
|
|
55
62
|
const after = current[current.length - 1]?.id;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ApiController, ApiRequestContext, ApiServerResult } from '../../../api/types.js';
|
|
2
2
|
import type { NotificationApiDefinition } from '../../api/notification.api.js';
|
|
3
3
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
4
|
+
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
4
5
|
import { NotificationService } from '../services/notification.service.js';
|
|
5
6
|
export declare class NotificationApiController implements ApiController<NotificationApiDefinition> {
|
|
6
7
|
protected readonly notificationService: NotificationService<Record<string, import("../../index.js").NotificationDefinition<import("../../../types/types.js").ObjectLiteral, import("../../../types/types.js").ObjectLiteral>>>;
|
|
8
|
+
protected readonly notificationTypeService: NotificationTypeService;
|
|
7
9
|
protected readonly sseService: NotificationSseService;
|
|
8
10
|
stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
11
|
+
types(): Promise<Record<string, string>>;
|
|
9
12
|
listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
|
|
10
13
|
markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
|
|
11
14
|
markAllRead({ getToken }: ApiRequestContext<NotificationApiDefinition, 'markAllRead'>): Promise<'ok'>;
|
|
@@ -10,9 +10,11 @@ import { toAsyncIterable } from '../../../rxjs-utils/index.js';
|
|
|
10
10
|
import { decodeBase64 } from '../../../utils/base64.js';
|
|
11
11
|
import { notificationApiDefinition } from '../../api/notification.api.js';
|
|
12
12
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
13
|
+
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
13
14
|
import { NotificationService } from '../services/notification.service.js';
|
|
14
15
|
let NotificationApiController = class NotificationApiController {
|
|
15
16
|
notificationService = inject(NotificationService);
|
|
17
|
+
notificationTypeService = inject(NotificationTypeService);
|
|
16
18
|
sseService = inject(NotificationSseService);
|
|
17
19
|
async *stream({ abortSignal, getToken }) {
|
|
18
20
|
const token = await getToken();
|
|
@@ -26,6 +28,9 @@ let NotificationApiController = class NotificationApiController {
|
|
|
26
28
|
this.sseService.unregister(token.payload.tenant, token.payload.subject, source);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
async types() {
|
|
32
|
+
return await this.notificationTypeService.getTypes();
|
|
33
|
+
}
|
|
29
34
|
async listInApp({ parameters, getToken }) {
|
|
30
35
|
const token = await getToken();
|
|
31
36
|
return await this.notificationService.listInApp(token.payload.tenant, token.payload.subject, parameters);
|
|
@@ -8,4 +8,5 @@ export type TypeInitializationData = {
|
|
|
8
8
|
export declare class NotificationTypeService extends Transactional {
|
|
9
9
|
readonly repository: import("../../../orm/server/index.js").EntityRepository<NotificationType>;
|
|
10
10
|
initializeTypes<T extends string>(typeData: Record<T, TypeInitializationData>): Promise<Record<T, NotificationType>>;
|
|
11
|
+
getTypes(): Promise<Record<string, string>>;
|
|
11
12
|
}
|
|
@@ -34,6 +34,11 @@ let NotificationTypeService = class NotificationTypeService extends Transactiona
|
|
|
34
34
|
const mappedTypes = typeEntries.map(([key]) => [key, assertDefinedPass(typeMap.get(key), 'Could not map notification type.')]);
|
|
35
35
|
return fromEntries(mappedTypes);
|
|
36
36
|
}
|
|
37
|
+
async getTypes() {
|
|
38
|
+
const types = await this.repository.loadAll();
|
|
39
|
+
const entries = types.map((type) => [type.key, type.label]);
|
|
40
|
+
return fromEntries(entries);
|
|
41
|
+
}
|
|
37
42
|
};
|
|
38
43
|
NotificationTypeService = __decorate([
|
|
39
44
|
Singleton()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterResolve } from '../../../injector/index.js';
|
|
1
2
|
import { type NewEntity } from '../../../orm/index.js';
|
|
2
3
|
import { Transactional, type Transaction } from '../../../orm/server/index.js';
|
|
3
4
|
import type { TypedOmit } from '../../../types/types.js';
|
|
@@ -5,6 +6,7 @@ import { NotificationPreference, type InAppNotificationView, type NotificationCh
|
|
|
5
6
|
export type NewNotificationData<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'timestamp' | 'status' | 'currentStep' | 'priority'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>;
|
|
6
7
|
export declare class NotificationService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
|
|
7
8
|
#private;
|
|
9
|
+
[afterResolve](): void;
|
|
8
10
|
send(tenantId: string, userId: string, notification: NewNotificationData<Definitions>, options?: {
|
|
9
11
|
transaction: Transaction;
|
|
10
12
|
}): Promise<void>;
|
|
@@ -7,8 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var _a;
|
|
8
8
|
var NotificationService_1;
|
|
9
9
|
import { and, desc, eq, isNull, lt, or } from 'drizzle-orm';
|
|
10
|
+
import { CancellationSignal } from '../../../cancellation/token.js';
|
|
10
11
|
import { BadRequestError } from '../../../errors/bad-request.error.js';
|
|
11
|
-
import { inject, Singleton } from '../../../injector/index.js';
|
|
12
|
+
import { afterResolve, inject, Singleton } from '../../../injector/index.js';
|
|
12
13
|
import { Logger } from '../../../logger/logger.js';
|
|
13
14
|
import { TRANSACTION_TIMESTAMP } from '../../../orm/index.js';
|
|
14
15
|
import { injectRepository, Transactional } from '../../../orm/server/index.js';
|
|
@@ -18,6 +19,7 @@ import { isDefined } from '../../../utils/type-guards.js';
|
|
|
18
19
|
import { InAppNotification, NotificationLogEntity, NotificationPreference, NotificationPriority, NotificationStatus, toInAppNotificationView, WebPushSubscription } from '../../models/index.js';
|
|
19
20
|
import { inAppNotification, notificationLog } from '../schemas.js';
|
|
20
21
|
import { NotificationAncillaryService } from './notification-ancillary.service.js';
|
|
22
|
+
import { NotificationDeliveryWorker } from './notification-delivery.worker.js';
|
|
21
23
|
import { NotificationSseService } from './notification-sse.service.js';
|
|
22
24
|
let NotificationService = NotificationService_1 = class NotificationService extends Transactional {
|
|
23
25
|
#notificationLogRepository = injectRepository(NotificationLogEntity);
|
|
@@ -25,9 +27,14 @@ let NotificationService = NotificationService_1 = class NotificationService exte
|
|
|
25
27
|
#preferenceRepository = injectRepository(NotificationPreference);
|
|
26
28
|
#webPushSubscriptionRepository = injectRepository(WebPushSubscription);
|
|
27
29
|
#notificationAncillaryService = inject(NotificationAncillaryService);
|
|
30
|
+
#deliveryWorker = inject(NotificationDeliveryWorker);
|
|
28
31
|
#taskQueue = inject((TaskQueue), 'notification');
|
|
29
32
|
#sseService = inject(NotificationSseService);
|
|
30
33
|
#logger = inject(Logger, NotificationService_1.name);
|
|
34
|
+
#cancellationSignal = inject(CancellationSignal);
|
|
35
|
+
[afterResolve]() {
|
|
36
|
+
this.#deliveryWorker.run(this.#cancellationSignal);
|
|
37
|
+
}
|
|
31
38
|
async send(tenantId, userId, notification, options) {
|
|
32
39
|
await this.useTransaction(options?.transaction, async (tx) => {
|
|
33
40
|
const notificationToInsert = {
|
|
@@ -135,7 +142,7 @@ let NotificationService = NotificationService_1 = class NotificationService exte
|
|
|
135
142
|
async dispatchUnreadCountUpdate(tenantId, userId) {
|
|
136
143
|
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
137
144
|
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
138
|
-
this.#sseService.dispatchUnreadCountUpdate(tenantId, userId, unreadCount);
|
|
145
|
+
await this.#sseService.dispatchUnreadCountUpdate(tenantId, userId, unreadCount);
|
|
139
146
|
});
|
|
140
147
|
}
|
|
141
148
|
};
|
|
@@ -5,6 +5,7 @@ import { setupIntegrationTest, truncateTables } from '../../unit-test/index.js';
|
|
|
5
5
|
import { NotificationChannel } from '../models/index.js';
|
|
6
6
|
import { NotificationApiController } from '../server/api/notification.api-controller.js';
|
|
7
7
|
import { NotificationSseService } from '../server/services/notification-sse.service.js';
|
|
8
|
+
import { NotificationTypeService } from '../server/services/notification-type.service.js';
|
|
8
9
|
import { NotificationService } from '../server/services/notification.service.js';
|
|
9
10
|
describe('Notification API (Integration)', () => {
|
|
10
11
|
let injector;
|
|
@@ -64,6 +65,13 @@ describe('Notification API (Integration)', () => {
|
|
|
64
65
|
await nextPromise;
|
|
65
66
|
expect(registerSpy).toHaveBeenCalledWith(tenantId, userId);
|
|
66
67
|
});
|
|
68
|
+
test('types should call service', async () => {
|
|
69
|
+
const notificationTypeService = injector.resolve(NotificationTypeService);
|
|
70
|
+
const getTypesSpy = vi.spyOn(notificationTypeService, 'getTypes').mockResolvedValue({ test: 'Test' });
|
|
71
|
+
const result = await controller.types();
|
|
72
|
+
expect(result).toEqual({ test: 'Test' });
|
|
73
|
+
expect(getTypesSpy).toHaveBeenCalled();
|
|
74
|
+
});
|
|
67
75
|
test('listInApp should call service', async () => {
|
|
68
76
|
const listInAppSpy = vi.spyOn(notificationService, 'listInApp').mockResolvedValue([]);
|
|
69
77
|
const params = { limit: 10, offset: 0, includeArchived: false };
|
|
@@ -17,4 +17,13 @@ describe('NotificationSseService', () => {
|
|
|
17
17
|
await expect(service.send(msg, 1)).resolves.not.toThrow();
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
|
+
test('should dispatch unread count update', async () => {
|
|
21
|
+
const { injector } = await setupIntegrationTest({ modules: { messageBus: true, signals: true } });
|
|
22
|
+
const service = injector.resolve(NotificationSseService);
|
|
23
|
+
const tenantId = 't1';
|
|
24
|
+
const userId = 'u1';
|
|
25
|
+
await runInInjectionContext(injector, async () => {
|
|
26
|
+
await expect(service.dispatchUnreadCountUpdate(tenantId, userId, 5)).resolves.not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
20
29
|
});
|
|
@@ -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
|
+
});
|
|
@@ -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,8 @@ 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
|
+
isBadRequestError(error: unknown): boolean;
|
|
34
|
+
isError(error: unknown, ...names: string[]): boolean;
|
|
31
35
|
}
|