@tstdl/base 0.93.104 → 0.93.106

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 (41) hide show
  1. package/api/server/gateway.d.ts +2 -1
  2. package/api/server/gateway.js +3 -2
  3. package/api/types.d.ts +3 -2
  4. package/document-management/server/services/document-file.service.js +2 -2
  5. package/http/server/http-server.d.ts +1 -0
  6. package/http/server/node/node-http-server.js +3 -0
  7. package/notification/api/notification.api.d.ts +13 -3
  8. package/notification/api/notification.api.js +9 -2
  9. package/notification/models/in-app-notification.model.d.ts +4 -4
  10. package/notification/models/notification-log.model.d.ts +15 -3
  11. package/notification/models/notification-log.model.js +8 -1
  12. package/notification/server/api/notification.api-controller.d.ts +3 -4
  13. package/notification/server/api/notification.api-controller.js +11 -2
  14. package/notification/server/drizzle/{0000_shiny_the_anarchist.sql → 0000_ancient_hellion.sql} +2 -2
  15. package/notification/server/drizzle/meta/0000_snapshot.json +3 -3
  16. package/notification/server/drizzle/meta/_journal.json +2 -2
  17. package/notification/server/module.d.ts +3 -1
  18. package/notification/server/module.js +6 -0
  19. package/notification/server/providers/channel-provider.d.ts +2 -2
  20. package/notification/server/providers/email-channel-provider.d.ts +2 -2
  21. package/notification/server/providers/in-app-channel-provider.d.ts +2 -2
  22. package/notification/server/providers/web-push-channel-provider.d.ts +2 -2
  23. package/notification/server/services/index.d.ts +1 -0
  24. package/notification/server/services/index.js +1 -0
  25. package/notification/server/services/notification-ancillary.service.d.ts +5 -0
  26. package/notification/server/services/notification-ancillary.service.js +14 -0
  27. package/notification/server/services/notification-delivery.worker.js +6 -2
  28. package/notification/server/services/notification-sse.service.d.ts +3 -2
  29. package/notification/server/services/notification-sse.service.js +18 -23
  30. package/notification/server/services/notification-template.service.d.ts +2 -2
  31. package/notification/server/services/notification.service.d.ts +5 -3
  32. package/notification/server/services/notification.service.js +10 -3
  33. package/notification/tests/notification-api.test.js +14 -7
  34. package/notification/tests/notification-flow.test.js +32 -23
  35. package/package.json +5 -5
  36. package/rxjs-utils/async-iterable.d.ts +2 -0
  37. package/rxjs-utils/async-iterable.js +15 -0
  38. package/rxjs-utils/index.d.ts +1 -0
  39. package/rxjs-utils/index.js +1 -0
  40. package/test5.d.ts +12 -0
  41. package/test5.js +10 -10
@@ -14,6 +14,7 @@ export type ApiGatewayMiddlewareContext = {
14
14
  readonly resourcePatternResult: URLPatternResult;
15
15
  readonly request: HttpServerRequest;
16
16
  readonly response: HttpServerResponse;
17
+ readonly abortSignal: AbortSignal;
17
18
  };
18
19
  export type ApiGatewayMiddlewareNext = AsyncMiddlewareNext;
19
20
  export type ApiGatewayMiddleware = AsyncMiddleware<ApiGatewayMiddlewareContext>;
@@ -59,7 +60,7 @@ export declare class ApiGateway implements Resolvable<ApiGatewayOptions> {
59
60
  addMiddleware(middleware: ApiGatewayMiddleware): void;
60
61
  supressErrors(...errorTypes: Type<Error>[]): void;
61
62
  registerApi<T extends ApiDefinition>(definition: ApiDefinition, implementation: ApiController<T>): void;
62
- handleHttpServerRequestContext({ request, respond, close }: HttpServerRequestContext): Promise<void>;
63
+ handleHttpServerRequestContext({ request, abortSignal, respond, close }: HttpServerRequestContext): Promise<void>;
63
64
  getApiMetadata(resource: URL): ApiMetadata;
64
65
  private updateMiddleware;
65
66
  private endpointMiddleware;
@@ -121,13 +121,13 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
121
121
  }
122
122
  }
123
123
  }
124
- async handleHttpServerRequestContext({ request, respond, close }) {
124
+ async handleHttpServerRequestContext({ request, abortSignal, respond, close }) {
125
125
  let responded = false;
126
126
  const response = new HttpServerResponse();
127
127
  try {
128
128
  const { api, patternResult } = this.getApiMetadata(request.url);
129
129
  const endpoint = api.endpoints.get(request.method);
130
- const context = { api, resourcePatternResult: patternResult, endpoint, request, response };
130
+ const context = { api, resourcePatternResult: patternResult, endpoint, request, response, abortSignal };
131
131
  await this.#composedMiddleware(context);
132
132
  responded = true;
133
133
  await respond(context.response);
@@ -198,6 +198,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
198
198
  parameters: validatedParameters,
199
199
  body,
200
200
  request: context.request,
201
+ abortSignal: context.abortSignal,
201
202
  tryGetToken: async () => {
202
203
  return await requestTokenProvider.tryGetToken(requestContext);
203
204
  },
package/api/types.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { Token } from '../authentication/index.js';
4
4
  import type { HttpServerRequest, HttpServerResponse } from '../http/server/index.js';
5
5
  import type { HttpMethod } from '../http/types.js';
6
6
  import type { SchemaOutput, SchemaTestable } from '../schema/index.js';
7
- import type { DataStream } from '../sse/index.js';
7
+ import type { DataStream, DataStreamSource } from '../sse/index.js';
8
8
  import type { ServerSentEventsSource } from '../sse/server-sent-events-source.js';
9
9
  import type { ServerSentEvents } from '../sse/server-sent-events.js';
10
10
  import type { NonUndefinable, OneOrMany, Record, ReturnTypeOrT } from '../types/index.js';
@@ -95,7 +95,7 @@ export type ApiEndpointParametersSchema<T extends ApiDefinition, K extends ApiEn
95
95
  export type ApiEndpointBodySchema<T extends ApiDefinition, K extends ApiEndpointKeys<T>> = NonUndefinable<ApiEndpoint<T, K>['body']>;
96
96
  export type ApiEndpointResultSchema<T extends ApiDefinition, K extends ApiEndpointKeys<T>> = NonUndefinable<ApiEndpoint<T, K>['result']>;
97
97
  export type ApiBinaryType = typeof Uint8Array | typeof Blob | typeof ReadableStream;
98
- export type ApiInputType<T extends SchemaTestable> = T extends ApiBinaryType ? InstanceType<Exclude<ApiBinaryType, typeof ReadableStream> | typeof ReadableStream<Uint8Array<ArrayBuffer>>> : T extends typeof ServerSentEvents ? ServerSentEventsSource : T extends typeof DataStream<infer U> ? AsyncIterable<U> : T extends SchemaTestable ? SchemaOutput<T> : never;
98
+ export type ApiInputType<T extends SchemaTestable> = T extends ApiBinaryType ? InstanceType<Exclude<ApiBinaryType, typeof ReadableStream> | typeof ReadableStream<Uint8Array<ArrayBuffer>>> : T extends typeof ServerSentEvents ? ServerSentEventsSource : T extends typeof DataStream<infer U> ? AsyncIterable<U> | DataStreamSource<U> : T extends SchemaTestable ? SchemaOutput<T> : never;
99
99
  export type ApiOutputType<T extends SchemaTestable> = T extends typeof ReadableStream ? ReadableStream<Uint8Array<ArrayBuffer>> : T extends typeof DataStream<infer U> ? Observable<U> : T extends SchemaTestable ? SchemaOutput<T> : never;
100
100
  export type ApiParameters<T extends ApiDefinition, K extends ApiEndpointKeys<T>> = ApiInputType<ApiEndpointParametersSchema<T, K>>;
101
101
  export type ApiClientBody<T extends ApiDefinition, K extends ApiEndpointKeys<T>> = ApiInputType<ApiEndpointBodySchema<T, K>>;
@@ -106,6 +106,7 @@ export type ApiRequestData<T extends ApiDefinition = ApiDefinition, K extends Ap
106
106
  parameters: ApiParameters<T, K>;
107
107
  body: ApiServerBody<T, K>;
108
108
  request: HttpServerRequest;
109
+ abortSignal: AbortSignal;
109
110
  };
110
111
  export type ApiRequestContext<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = ApiRequestData<T, K> & {
111
112
  tryGetToken<T extends Token>(): Promise<T | null>;
@@ -73,7 +73,7 @@ import { currentTimestamp } from '../../../utils/date-time.js';
73
73
  import { getRandomString } from '../../../utils/random.js';
74
74
  import { readableStreamFromPromise, readBinaryStream } from '../../../utils/stream/index.js';
75
75
  import { isDefined, isNotReadableStream, isNotUint8Array, isUint8Array } from '../../../utils/type-guards.js';
76
- import { millisecondsPerMinute, secondsPerMinute } from '../../../utils/units.js';
76
+ import { millisecondsPerMinute } from '../../../utils/units.js';
77
77
  import { Document } from '../../models/index.js';
78
78
  import { DocumentManagementConfiguration } from '../module.js';
79
79
  import { DocumentManagementSingleton } from './singleton.js';
@@ -82,7 +82,7 @@ let DocumentFileService = class DocumentFileService extends Transactional {
82
82
  #aiService = inject(AiService);
83
83
  #fileObjectStorage = inject(ObjectStorage, this.#configuration.fileObjectStorageModule);
84
84
  #filePreviewObjectStorage = inject(ObjectStorage, this.#configuration.filePreviewObjectStorageModule);
85
- #fileUploadObjectStorage = inject(ObjectStorage, { module: this.#configuration.fileUploadObjectStorageModule, configuration: { lifecycle: { expiration: { after: 5 * secondsPerMinute } } } });
85
+ #fileUploadObjectStorage = inject(ObjectStorage, { module: this.#configuration.fileUploadObjectStorageModule /* , configuration: { lifecycle: { expiration: { after: 5 * secondsPerMinute } } } */ });
86
86
  #aiFilePartCache = new Map();
87
87
  /**
88
88
  * Initiates a file upload
@@ -1,6 +1,7 @@
1
1
  import type { HttpServerRequest, HttpServerResponse } from '../../http/server/index.js';
2
2
  export type HttpServerRequestContext<Context = unknown> = {
3
3
  request: HttpServerRequest;
4
+ abortSignal: AbortSignal;
4
5
  context: Context;
5
6
  respond(response: HttpServerResponse): Promise<void>;
6
7
  close(): Promise<void>;
@@ -116,8 +116,11 @@ let NodeHttpServer = NodeHttpServer_1 = class NodeHttpServer extends HttpServer
116
116
  ip: clientIp,
117
117
  body: request,
118
118
  });
119
+ const abortController = new AbortController();
120
+ response.on('close', () => abortController.abort());
119
121
  const item = {
120
122
  request: httpRequest,
123
+ abortSignal: abortController.signal,
121
124
  respond: getResponder(response),
122
125
  context,
123
126
  async close() {
@@ -1,4 +1,4 @@
1
- import { ServerSentEvents } from '../../sse/server-sent-events.js';
1
+ import { DataStream } from '../../sse/data-stream.js';
2
2
  import { InAppNotificationView } from '../models/in-app-notification.model.js';
3
3
  export declare const notificationApiDefinition: {
4
4
  resource: string;
@@ -6,7 +6,11 @@ export declare const notificationApiDefinition: {
6
6
  stream: {
7
7
  resource: string;
8
8
  method: "GET";
9
- result: typeof ServerSentEvents;
9
+ result: {
10
+ new (): DataStream<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
11
+ parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
12
+ };
13
+ credentials: true;
10
14
  };
11
15
  listInApp: {
12
16
  resource: string;
@@ -16,7 +20,8 @@ export declare const notificationApiDefinition: {
16
20
  limit?: number | undefined;
17
21
  includeArchived?: boolean | undefined;
18
22
  }>;
19
- result: import("../../schema/index.js").ArraySchema<InAppNotificationView>;
23
+ result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
24
+ credentials: true;
20
25
  };
21
26
  markRead: {
22
27
  resource: string;
@@ -25,6 +30,7 @@ export declare const notificationApiDefinition: {
25
30
  id: string;
26
31
  }>;
27
32
  result: import("../../schema/index.js").LiteralSchema<"ok">;
33
+ credentials: true;
28
34
  };
29
35
  archive: {
30
36
  resource: string;
@@ -33,6 +39,7 @@ export declare const notificationApiDefinition: {
33
39
  id: string;
34
40
  }>;
35
41
  result: import("../../schema/index.js").LiteralSchema<"ok">;
42
+ credentials: true;
36
43
  };
37
44
  getPreferences: {
38
45
  resource: string;
@@ -42,6 +49,7 @@ export declare const notificationApiDefinition: {
42
49
  enabled: boolean;
43
50
  channel: "email" | "in-app" | "web-push";
44
51
  }>;
52
+ credentials: true;
45
53
  };
46
54
  updatePreference: {
47
55
  resource: string;
@@ -52,6 +60,7 @@ export declare const notificationApiDefinition: {
52
60
  readonly enabled: boolean;
53
61
  }>;
54
62
  result: import("../../schema/index.js").LiteralSchema<"ok">;
63
+ credentials: true;
55
64
  };
56
65
  registerWebPush: {
57
66
  resource: string;
@@ -64,6 +73,7 @@ export declare const notificationApiDefinition: {
64
73
  };
65
74
  }>;
66
75
  result: import("../../schema/index.js").LiteralSchema<"ok">;
76
+ credentials: true;
67
77
  };
68
78
  };
69
79
  };
@@ -1,6 +1,6 @@
1
1
  import { defineApi } from '../../api/types.js';
2
2
  import { array, boolean, enumeration, explicitObject, literal, number, object, optional, string } from '../../schema/index.js';
3
- import { ServerSentEvents } from '../../sse/server-sent-events.js';
3
+ import { DataStream } from '../../sse/data-stream.js';
4
4
  import { InAppNotificationView } from '../models/in-app-notification.model.js';
5
5
  import { NotificationChannel } from '../models/index.js';
6
6
  export const notificationApiDefinition = defineApi({
@@ -9,7 +9,8 @@ export const notificationApiDefinition = defineApi({
9
9
  stream: {
10
10
  resource: 'stream',
11
11
  method: 'GET',
12
- result: ServerSentEvents,
12
+ result: (DataStream),
13
+ credentials: true,
13
14
  },
14
15
  listInApp: {
15
16
  resource: 'in-app',
@@ -20,18 +21,21 @@ export const notificationApiDefinition = defineApi({
20
21
  includeArchived: optional(boolean()),
21
22
  }),
22
23
  result: array(InAppNotificationView),
24
+ credentials: true,
23
25
  },
24
26
  markRead: {
25
27
  resource: 'in-app/:id/read',
26
28
  method: 'POST',
27
29
  parameters: object({ id: string() }),
28
30
  result: literal('ok'),
31
+ credentials: true,
29
32
  },
30
33
  archive: {
31
34
  resource: 'in-app/:id/archive',
32
35
  method: 'POST',
33
36
  parameters: object({ id: string() }),
34
37
  result: literal('ok'),
38
+ credentials: true,
35
39
  },
36
40
  getPreferences: {
37
41
  resource: 'preferences',
@@ -41,6 +45,7 @@ export const notificationApiDefinition = defineApi({
41
45
  channel: enumeration(NotificationChannel),
42
46
  enabled: boolean(),
43
47
  })),
48
+ credentials: true,
44
49
  },
45
50
  updatePreference: {
46
51
  resource: 'preferences',
@@ -51,6 +56,7 @@ export const notificationApiDefinition = defineApi({
51
56
  enabled: boolean(),
52
57
  }),
53
58
  result: literal('ok'),
59
+ credentials: true,
54
60
  },
55
61
  registerWebPush: {
56
62
  resource: 'web-push/register',
@@ -63,6 +69,7 @@ export const notificationApiDefinition = defineApi({
63
69
  }),
64
70
  }),
65
71
  result: literal('ok'),
72
+ credentials: true,
66
73
  },
67
74
  },
68
75
  });
@@ -1,6 +1,6 @@
1
1
  import { TenantBaseEntity, type Uuid } from '../../orm/index.js';
2
2
  import type { Timestamp } from '../../orm/types.js';
3
- import { type NotificationLog } from './notification-log.model.js';
3
+ import { type NotificationDefinitionMap, type NotificationLogView } from './notification-log.model.js';
4
4
  export declare class InAppNotification extends TenantBaseEntity {
5
5
  static readonly entityName = "InAppNotification";
6
6
  userId: Uuid;
@@ -8,7 +8,7 @@ export declare class InAppNotification extends TenantBaseEntity {
8
8
  readTimestamp: Timestamp | null;
9
9
  archiveTimestamp: Timestamp | null;
10
10
  }
11
- export declare class InAppNotificationView extends InAppNotification {
12
- notification: NotificationLog;
11
+ export declare class InAppNotificationView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends InAppNotification {
12
+ notification: NotificationLogView<Definitions>;
13
13
  }
14
- export declare function toInAppNotificationView(inAppNotification: InAppNotification, notification: NotificationLog): InAppNotificationView;
14
+ export declare function toInAppNotificationView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap>(inAppNotification: InAppNotification, notification: NotificationLogView<Definitions>): InAppNotificationView<Definitions>;
@@ -22,12 +22,14 @@ export declare const NotificationStatus: {
22
22
  readonly Failed: "failed";
23
23
  };
24
24
  export type NotificationStatus = EnumType<typeof NotificationStatus>;
25
- export type NotificationDefinition<Payload extends ObjectLiteral = ObjectLiteral> = {
26
- payload: Payload;
25
+ export type NotificationDefinition<Payload extends ObjectLiteral = ObjectLiteral, View extends ObjectLiteral = ObjectLiteral> = {
26
+ payload?: Payload;
27
+ view?: View;
27
28
  };
28
29
  export type NotificationDefinitionMap<Definition extends Record<string, NotificationDefinition> = Record<string, NotificationDefinition>> = Definition;
29
30
  export type NotificationTypes<Definitions extends NotificationDefinitionMap> = Extract<keyof Definitions, string>;
30
- export type NotificationPayload<Definitions extends NotificationDefinitionMap, Type extends NotificationTypes<Definitions>> = Definitions[Type]['payload'];
31
+ export type NotificationPayload<Definitions extends NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> = Definitions[Type]['payload'];
32
+ export type NotificationView<Definitions extends NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> = Definitions[Type]['view'];
31
33
  export type NotificationOfType<Definitions extends NotificationDefinitionMap, Type extends NotificationTypes<Definitions>> = string extends NotificationTypes<Definitions> ? NotificationLog<Definitions> & {
32
34
  type: Type;
33
35
  } : Extract<NotificationLog<Definitions>, {
@@ -39,6 +41,13 @@ export type NotificationLog<Definitions extends NotificationDefinitionMap = Noti
39
41
  payload: Json<NotificationPayload<Definitions, Type>> | null;
40
42
  };
41
43
  }[NotificationTypes<Definitions>];
44
+ export type NotificationLogView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
45
+ [Type in NotificationTypes<Definitions>]: Omit<NotificationLogEntity<Definitions, Type>, 'type' | 'payload'> & {
46
+ type: Type;
47
+ payload: NotificationPayload<Definitions, Type>;
48
+ view: NotificationView<Definitions, Type>;
49
+ };
50
+ }[NotificationTypes<Definitions>];
42
51
  export declare class NotificationLogEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends TenantEntity {
43
52
  static readonly entityName = "NotificationLog";
44
53
  userId: Uuid;
@@ -48,3 +57,6 @@ export declare class NotificationLogEntity<Definitions extends NotificationDefin
48
57
  currentStep: number;
49
58
  payload: Json<NotificationPayload<Definitions, Type>> | null;
50
59
  }
60
+ export declare class NotificationLogViewEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends NotificationLogEntity<Definitions, Type> {
61
+ view?: unknown;
62
+ }
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  import { User } from '../../authentication/models/user.model.js';
11
11
  import { defineEnum } from '../../enumeration/index.js';
12
12
  import { JsonProperty, Reference, TenantEntity, TenantReference, Unique, UuidProperty } from '../../orm/index.js';
13
- import { Enumeration, Integer, StringProperty } from '../../schema/index.js';
13
+ import { Enumeration, Integer, StringProperty, Unknown } from '../../schema/index.js';
14
14
  import { NotificationTable } from './notification-table.js';
15
15
  import { NotificationType } from './notification-type.model.js';
16
16
  export const NotificationChannel = defineEnum('NotificationChannel', {
@@ -71,3 +71,10 @@ NotificationLogEntity = __decorate([
71
71
  Unique(['tenantId', 'id', 'userId'])
72
72
  ], NotificationLogEntity);
73
73
  export { NotificationLogEntity };
74
+ export class NotificationLogViewEntity extends NotificationLogEntity {
75
+ view;
76
+ }
77
+ __decorate([
78
+ Unknown(),
79
+ __metadata("design:type", Object)
80
+ ], NotificationLogViewEntity.prototype, "view", void 0);
@@ -1,12 +1,11 @@
1
- import type { ApiController, ApiRequestContext } from '../../../api/types.js';
2
- import type { ServerSentEventsSource } from '../../../sse/server-sent-events-source.js';
1
+ import type { ApiController, ApiRequestContext, ApiServerResult } from '../../../api/types.js';
3
2
  import type { NotificationApiDefinition } from '../../api/notification.api.js';
4
3
  import { NotificationSseService } from '../services/notification-sse.service.js';
5
4
  import { NotificationService } from '../services/notification.service.js';
6
5
  export declare class NotificationApiController implements ApiController<NotificationApiDefinition> {
7
- protected readonly notificationService: NotificationService;
6
+ protected readonly notificationService: NotificationService<Record<string, import("../../index.js").NotificationDefinition<import("../../../types/types.js").ObjectLiteral, import("../../../types/types.js").ObjectLiteral>>>;
8
7
  protected readonly sseService: NotificationSseService;
9
- stream({ getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): Promise<ServerSentEventsSource>;
8
+ stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
10
9
  listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
11
10
  markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
12
11
  archive({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'archive'>): Promise<'ok'>;
@@ -6,6 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { apiController } from '../../../api/server/index.js';
8
8
  import { inject } from '../../../injector/index.js';
9
+ import { toAsyncIterable } from '../../../rxjs-utils/index.js';
9
10
  import { decodeBase64 } from '../../../utils/base64.js';
10
11
  import { notificationApiDefinition } from '../../api/notification.api.js';
11
12
  import { NotificationSseService } from '../services/notification-sse.service.js';
@@ -13,9 +14,17 @@ import { NotificationService } from '../services/notification.service.js';
13
14
  let NotificationApiController = class NotificationApiController {
14
15
  notificationService = inject(NotificationService);
15
16
  sseService = inject(NotificationSseService);
16
- async stream({ getToken }) {
17
+ async *stream({ abortSignal, getToken }) {
17
18
  const token = await getToken();
18
- return this.sseService.register(token.payload.tenant, token.payload.subject);
19
+ const source = this.sseService.register(token.payload.tenant, token.payload.subject);
20
+ const asyncIterable = toAsyncIterable(source);
21
+ abortSignal.addEventListener('abort', () => this.sseService.unregister(token.payload.tenant, token.payload.subject, source));
22
+ try {
23
+ yield* asyncIterable;
24
+ }
25
+ finally {
26
+ this.sseService.unregister(token.payload.tenant, token.payload.subject, source);
27
+ }
19
28
  }
20
29
  async listInApp({ parameters, getToken }) {
21
30
  const token = await getToken();
@@ -64,8 +64,8 @@ CREATE TABLE "notification"."web_push_subscription" (
64
64
  "tenant_id" uuid NOT NULL,
65
65
  "user_id" uuid NOT NULL,
66
66
  "endpoint" text NOT NULL,
67
- "p256dh" text NOT NULL,
68
- "auth" text NOT NULL,
67
+ "p256dh" "bytea" NOT NULL,
68
+ "auth" "bytea" NOT NULL,
69
69
  "revision" integer NOT NULL,
70
70
  "revision_timestamp" timestamp with time zone NOT NULL,
71
71
  "create_timestamp" timestamp with time zone NOT NULL,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "8f352123-edd8-491f-a4ad-008d09531722",
2
+ "id": "7751e29f-1334-4511-8bf5-0d0f2550b5ae",
3
3
  "prevId": "00000000-0000-0000-0000-000000000000",
4
4
  "version": "7",
5
5
  "dialect": "postgresql",
@@ -521,13 +521,13 @@
521
521
  },
522
522
  "p256dh": {
523
523
  "name": "p256dh",
524
- "type": "text",
524
+ "type": "bytea",
525
525
  "primaryKey": false,
526
526
  "notNull": true
527
527
  },
528
528
  "auth": {
529
529
  "name": "auth",
530
- "type": "text",
530
+ "type": "bytea",
531
531
  "primaryKey": false,
532
532
  "notNull": true
533
533
  },
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1769546272361,
9
- "tag": "0000_shiny_the_anarchist",
8
+ "when": 1770118617281,
9
+ "tag": "0000_ancient_hellion",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -1,9 +1,11 @@
1
- import { Injector } from '../../injector/index.js';
1
+ import { Injector, type InjectionToken } from '../../injector/index.js';
2
2
  import { type DatabaseConfig } from '../../orm/server/index.js';
3
3
  import type { NotificationChannel } from '../models/index.js';
4
+ import { NotificationAncillaryService } from './services/notification-ancillary.service.js';
4
5
  export declare class NotificationConfiguration {
5
6
  database?: DatabaseConfig;
6
7
  defaultChannels?: NotificationChannel[];
8
+ ancillaryService?: InjectionToken<NotificationAncillaryService>;
7
9
  }
8
10
  export declare function configureNotification({ injector, ...config }: NotificationConfiguration & {
9
11
  injector?: Injector;
@@ -1,12 +1,18 @@
1
1
  import { inject, Injector } from '../../injector/index.js';
2
2
  import { Database, migrate } from '../../orm/server/index.js';
3
+ import { isDefined } from '../../utils/type-guards.js';
4
+ import { NotificationAncillaryService } from './services/notification-ancillary.service.js';
3
5
  export class NotificationConfiguration {
4
6
  database;
5
7
  defaultChannels;
8
+ ancillaryService;
6
9
  }
7
10
  export function configureNotification({ injector, ...config }) {
8
11
  const targetInjector = injector ?? Injector;
9
12
  targetInjector.register(NotificationConfiguration, { useValue: config });
13
+ if (isDefined(config.ancillaryService)) {
14
+ targetInjector.register(NotificationAncillaryService, { useToken: config.ancillaryService });
15
+ }
10
16
  }
11
17
  /**
12
18
  * Migrates the notification schema.
@@ -1,5 +1,5 @@
1
1
  import type { Transaction } from '../../../orm/server/index.js';
2
- import type { NotificationLog } from '../../models/notification-log.model.js';
2
+ import type { NotificationLogView } from '../../models/notification-log.model.js';
3
3
  export declare abstract class ChannelProvider {
4
- abstract send(notification: NotificationLog, tx?: Transaction): Promise<void>;
4
+ abstract send(notification: NotificationLogView, tx?: Transaction): Promise<void>;
5
5
  }
@@ -1,6 +1,6 @@
1
- import type { NotificationLog } from '../../models/index.js';
1
+ import type { NotificationLogView } from '../../models/index.js';
2
2
  import { ChannelProvider } from './channel-provider.js';
3
3
  export declare class EmailChannelProvider extends ChannelProvider {
4
4
  #private;
5
- send(notification: NotificationLog): Promise<void>;
5
+ send(notification: NotificationLogView): Promise<void>;
6
6
  }
@@ -1,7 +1,7 @@
1
1
  import { type Transaction } from '../../../orm/server/index.js';
2
- import { type NotificationLog } from '../../models/index.js';
2
+ import { type NotificationLogView } from '../../models/index.js';
3
3
  import { ChannelProvider } from './channel-provider.js';
4
4
  export declare class InAppChannelProvider extends ChannelProvider {
5
5
  #private;
6
- send(notification: NotificationLog, tx?: Transaction): Promise<void>;
6
+ send(notification: NotificationLogView, tx?: Transaction): Promise<void>;
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { type Transaction } from '../../../orm/server/index.js';
2
- import { type NotificationLog } from '../../models/index.js';
2
+ import { type NotificationLogView } from '../../models/index.js';
3
3
  import { ChannelProvider } from './channel-provider.js';
4
4
  export declare class WebPushChannelProvider extends ChannelProvider {
5
5
  #private;
6
- send(notification: NotificationLog, tx?: Transaction): Promise<void>;
6
+ send(notification: NotificationLogView, tx?: Transaction): Promise<void>;
7
7
  }
@@ -1,3 +1,4 @@
1
+ export * from './notification-ancillary.service.js';
1
2
  export * from './notification-delivery.worker.js';
2
3
  export * from './notification-sse.service.js';
3
4
  export * from './notification-template.js';
@@ -1,3 +1,4 @@
1
+ export * from './notification-ancillary.service.js';
1
2
  export * from './notification-delivery.worker.js';
2
3
  export * from './notification-sse.service.js';
3
4
  export * from './notification-template.js';
@@ -0,0 +1,5 @@
1
+ import { Transactional } from '../../../orm/server/index.js';
2
+ import type { NotificationDefinitionMap, NotificationLog, NotificationView } from '../../models/index.js';
3
+ export declare abstract class NotificationAncillaryService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
4
+ abstract getViewData(notifications: NotificationLog<Definitions>[]): Promise<NotificationView<Definitions>[]>;
5
+ }
@@ -0,0 +1,14 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Singleton } from '../../../injector/index.js';
8
+ import { Transactional } from '../../../orm/server/index.js';
9
+ let NotificationAncillaryService = class NotificationAncillaryService extends Transactional {
10
+ };
11
+ NotificationAncillaryService = __decorate([
12
+ Singleton()
13
+ ], NotificationAncillaryService);
14
+ export { NotificationAncillaryService };
@@ -12,11 +12,13 @@ import { TaskProcessResult, TaskQueue } from '../../../task-queue/task-queue.js'
12
12
  import { isNotNull, isNotNullOrUndefined, isUndefined } from '../../../utils/type-guards.js';
13
13
  import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationStatus, NotificationType } from '../../models/index.js';
14
14
  import { NotificationConfiguration } from '../module.js';
15
+ import { NotificationAncillaryService } from './notification-ancillary.service.js';
15
16
  let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transactional {
16
17
  #notificationLogRepository = injectRepository(NotificationLogEntity);
17
18
  #inAppRepository = injectRepository(InAppNotification);
18
19
  #preferenceRepository = injectRepository(NotificationPreference);
19
20
  #typeRepository = injectRepository(NotificationType);
21
+ #notificationAncillaryService = inject(NotificationAncillaryService);
20
22
  #taskQueue = inject((TaskQueue), 'notification');
21
23
  #rateLimiter = inject(RateLimiter, 'notification');
22
24
  #logger = inject(Logger, 'NotificationDeliveryWorker');
@@ -77,7 +79,8 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
77
79
  }
78
80
  for (const channel of enabledChannels) {
79
81
  // TODO: what if an error occurs here? partial delivery? Should we use tasks to allow retrying individual channels?
80
- await this.sendToChannel(notification, channel, tx);
82
+ const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
83
+ await this.sendToChannel({ ...notification, view: viewData }, channel, tx);
81
84
  }
82
85
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { status: NotificationStatus.Sent, currentStep: 1 });
83
86
  if (isNotNull(type.escalations) && (type.escalations.length > 0)) {
@@ -95,7 +98,8 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
95
98
  this.#logger.debug(`Notification ${notificationId} already read, skipping escalation step ${step}`);
96
99
  return TaskProcessResult.Complete();
97
100
  }
98
- await this.sendToChannel(notification, rule.channel, tx);
101
+ const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
102
+ await this.sendToChannel({ ...notification, view: viewData }, rule.channel, tx);
99
103
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { currentStep: step + 1 });
100
104
  if (step < type.escalations.length) {
101
105
  const nextRule = type.escalations[step];
@@ -1,10 +1,11 @@
1
+ import { Subject } from 'rxjs';
1
2
  import { afterResolve } from '../../../injector/index.js';
2
- import { ServerSentEventsSource } from '../../../sse/server-sent-events-source.js';
3
3
  import type { InAppNotificationView } from '../../models/index.js';
4
4
  export declare class NotificationSseService {
5
5
  #private;
6
6
  [afterResolve](): void;
7
- register(tenantId: string, userId: string): ServerSentEventsSource;
7
+ register(tenantId: string, userId: string): Subject<InAppNotificationView>;
8
+ unregister(tenantId: string, userId: string, source: Subject<InAppNotificationView>): void;
8
9
  send(notification: InAppNotificationView): Promise<void>;
9
10
  private dispatchToLocal;
10
11
  }
@@ -4,10 +4,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ import { Subject } from 'rxjs';
7
8
  import { afterResolve, inject, Singleton } from '../../../injector/index.js';
8
- import { MessageBusProvider } from '../../../message-bus/message-bus-provider.js';
9
- import { toObservable } from '../../../signals/api.js';
10
- import { ServerSentEventsSource } from '../../../sse/server-sent-events-source.js';
9
+ import { MessageBusProvider } from '../../../message-bus/index.js';
11
10
  let NotificationSseService = class NotificationSseService {
12
11
  #messageBusProvider = inject(MessageBusProvider);
13
12
  #messageBus = this.#messageBusProvider.get('notification');
@@ -16,7 +15,7 @@ let NotificationSseService = class NotificationSseService {
16
15
  this.#messageBus.allMessages$.subscribe((message) => this.dispatchToLocal(message));
17
16
  }
18
17
  register(tenantId, userId) {
19
- const source = new ServerSentEventsSource();
18
+ const source = new Subject();
20
19
  let tenantMap = this.#sources.get(tenantId);
21
20
  if (tenantMap == null) {
22
21
  tenantMap = new Map();
@@ -28,23 +27,22 @@ let NotificationSseService = class NotificationSseService {
28
27
  tenantMap.set(userId, userSources);
29
28
  }
30
29
  userSources.add(source);
31
- toObservable(source.closed).subscribe((closed) => {
32
- if (closed) {
33
- const currentTenantMap = this.#sources.get(tenantId);
34
- const currentUserSources = currentTenantMap?.get(userId);
35
- if (currentUserSources) {
36
- currentUserSources.delete(source);
37
- if (currentUserSources.size == 0) {
38
- currentTenantMap?.delete(userId);
39
- }
40
- }
41
- if (currentTenantMap?.size == 0) {
42
- this.#sources.delete(tenantId);
43
- }
44
- }
45
- });
46
30
  return source;
47
31
  }
32
+ unregister(tenantId, userId, source) {
33
+ const tenantMap = this.#sources.get(tenantId);
34
+ const userSources = tenantMap?.get(userId);
35
+ if (userSources) {
36
+ userSources.delete(source);
37
+ if (userSources.size == 0) {
38
+ tenantMap?.delete(userId);
39
+ }
40
+ }
41
+ if (tenantMap?.size == 0) {
42
+ this.#sources.delete(tenantId);
43
+ }
44
+ source.complete();
45
+ }
48
46
  async send(notification) {
49
47
  await this.#messageBus.publish(notification);
50
48
  }
@@ -53,10 +51,7 @@ let NotificationSseService = class NotificationSseService {
53
51
  const userSources = tenantMap?.get(notification.userId);
54
52
  if (userSources != null) {
55
53
  for (const source of userSources) {
56
- void source.sendJson({
57
- name: 'notification',
58
- data: notification,
59
- });
54
+ source.next(notification);
60
55
  }
61
56
  }
62
57
  }
@@ -1,7 +1,7 @@
1
- import { type NotificationLog } from '../../models/index.js';
1
+ import { type NotificationLogView } from '../../models/index.js';
2
2
  import type { NotificationContent, NotificationTemplate } from './notification-template.js';
3
3
  export declare class NotificationTemplateService {
4
4
  #private;
5
5
  register(template: NotificationTemplate<any>): void;
6
- render(notification: NotificationLog, language?: string): Promise<NotificationContent>;
6
+ render(notification: NotificationLogView, language?: string): Promise<NotificationContent>;
7
7
  }
@@ -1,8 +1,10 @@
1
+ import { type NewEntity } from '../../../orm/index.js';
1
2
  import { Transactional } from '../../../orm/server/index.js';
2
- import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationLog } from '../../models/index.js';
3
- export declare class NotificationService extends Transactional {
3
+ import type { TypedOmit } from '../../../types/types.js';
4
+ import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationDefinitionMap, type NotificationLog } from '../../models/index.js';
5
+ export declare class NotificationService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
4
6
  #private;
5
- send(notification: NotificationLog): Promise<void>;
7
+ send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'status' | 'currentStep' | 'priority' | 'metadata'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>): Promise<void>;
6
8
  listInApp(tenantId: string, userId: string, options?: {
7
9
  limit?: number;
8
10
  offset?: number;
@@ -10,17 +10,22 @@ import { inject, Singleton } from '../../../injector/index.js';
10
10
  import { TRANSACTION_TIMESTAMP } from '../../../orm/index.js';
11
11
  import { injectRepository, Transactional } from '../../../orm/server/index.js';
12
12
  import { TaskQueue } from '../../../task-queue/task-queue.js';
13
- import { InAppNotification, NotificationLogEntity, NotificationPreference, NotificationStatus, toInAppNotificationView, WebPushSubscription } from '../../models/index.js';
13
+ import { InAppNotification, NotificationLogEntity, NotificationPreference, NotificationPriority, NotificationStatus, toInAppNotificationView, WebPushSubscription } from '../../models/index.js';
14
14
  import { inAppNotification, notificationLog } from '../schemas.js';
15
+ import { NotificationAncillaryService } from './notification-ancillary.service.js';
15
16
  let NotificationService = class NotificationService extends Transactional {
16
17
  #notificationLogRepository = injectRepository(NotificationLogEntity);
17
18
  #inAppNotificationRepository = injectRepository(InAppNotification);
18
19
  #preferenceRepository = injectRepository(NotificationPreference);
19
20
  #webPushSubscriptionRepository = injectRepository(WebPushSubscription);
21
+ #notificationAncillaryService = inject(NotificationAncillaryService);
20
22
  #taskQueue = inject((TaskQueue), 'notification');
21
- async send(notification) {
23
+ async send(tenantId, userId, notification) {
22
24
  await this.transaction(async (tx) => {
23
25
  const notificationToInsert = {
26
+ tenantId,
27
+ userId,
28
+ priority: NotificationPriority.Medium,
24
29
  ...notification,
25
30
  status: NotificationStatus.Pending,
26
31
  currentStep: 0,
@@ -45,9 +50,11 @@ let NotificationService = class NotificationService extends Transactional {
45
50
  const notificationRows = rows.map((row) => row.notification);
46
51
  const notificationEntities = await this.#notificationLogRepository.mapManyToEntity(notificationRows);
47
52
  const inAppEntities = await this.#inAppNotificationRepository.mapManyToEntity(inAppRows);
53
+ const notificationViewDatas = await this.#notificationAncillaryService.getViewData(notificationEntities);
48
54
  const views = notificationEntities.map((notification, index) => {
49
55
  const inApp = inAppEntities[index];
50
- return toInAppNotificationView(inApp, notification);
56
+ const viewData = notificationViewDatas[index];
57
+ return toInAppNotificationView(inApp, { ...notification, view: viewData });
51
58
  });
52
59
  return views;
53
60
  }
@@ -1,13 +1,14 @@
1
+ import { Subject } from 'rxjs';
1
2
  import { beforeAll, describe, expect, test, vi } from 'vitest';
2
3
  import { SubjectService } from '../../authentication/server/subject.service.js';
3
- import {} from '../../injector/index.js';
4
- import { setupIntegrationTest } from '../../unit-test/index.js';
4
+ 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
8
  import { NotificationService } from '../server/services/notification.service.js';
9
9
  describe('Notification API (Integration)', () => {
10
10
  let injector;
11
+ let database;
11
12
  let controller;
12
13
  let notificationService;
13
14
  let sseService;
@@ -16,7 +17,7 @@ describe('Notification API (Integration)', () => {
16
17
  const tenantId = '00000000-0000-0000-0000-000000000000';
17
18
  let userId;
18
19
  beforeAll(async () => {
19
- ({ injector } = await setupIntegrationTest({
20
+ ({ injector, database } = await setupIntegrationTest({
20
21
  orm: { schema },
21
22
  modules: {
22
23
  notification: true,
@@ -25,6 +26,7 @@ describe('Notification API (Integration)', () => {
25
26
  rateLimiter: true,
26
27
  },
27
28
  }));
29
+ await truncateTables(database, 'authentication', ['user', 'subject']);
28
30
  controller = injector.resolve(NotificationApiController);
29
31
  notificationService = injector.resolve(NotificationService);
30
32
  sseService = injector.resolve(NotificationSseService);
@@ -40,6 +42,7 @@ describe('Notification API (Integration)', () => {
40
42
  });
41
43
  const createMockContext = (params = {}) => ({
42
44
  parameters: params,
45
+ abortSignal: new AbortController().signal,
43
46
  getToken: async () => ({
44
47
  payload: {
45
48
  tenant: tenantId,
@@ -49,12 +52,16 @@ describe('Notification API (Integration)', () => {
49
52
  });
50
53
  test('stream should register sse client', async () => {
51
54
  const registerSpy = vi.spyOn(sseService, 'register');
55
+ const subject = new Subject();
52
56
  // Mock register to return a dummy source or similar if needed,
53
57
  // but the real one might work if it just returns an object.
54
58
  // Looking at sseService, register returns a ServerSentEventsSource.
55
59
  // We might need to mock implementation if it tries to do something complex
56
- registerSpy.mockImplementation(() => ({}));
57
- await controller.stream(createMockContext());
60
+ registerSpy.mockImplementation(() => subject);
61
+ const generator = controller.stream(createMockContext());
62
+ const nextPromise = generator.next();
63
+ subject.complete();
64
+ await nextPromise;
58
65
  expect(registerSpy).toHaveBeenCalledWith(tenantId, userId);
59
66
  });
60
67
  test('listInApp should call service', async () => {
@@ -88,8 +95,8 @@ describe('Notification API (Integration)', () => {
88
95
  });
89
96
  test('registerWebPush should call service', async () => {
90
97
  const registerWebPushSpy = vi.spyOn(notificationService, 'registerWebPush').mockResolvedValue();
91
- const params = { endpoint: 'url', keys: { p256dh: 'key', auth: 'auth' } };
98
+ const params = { endpoint: 'url', keys: { p256dhBase64: 'key', authBase64: 'auth' } };
92
99
  await controller.registerWebPush(createMockContext(params));
93
- expect(registerWebPushSpy).toHaveBeenCalledWith(tenantId, userId, 'url', 'key', 'auth');
100
+ expect(registerWebPushSpy).toHaveBeenCalledWith(tenantId, userId, 'url', expect.any(Uint8Array), expect.any(Uint8Array));
94
101
  });
95
102
  });
@@ -1,14 +1,30 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
1
7
  import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2
8
  import { SubjectService } from '../../authentication/server/subject.service.js';
3
- import { runInInjectionContext } from '../../injector/index.js';
9
+ import { runInInjectionContext, Singleton } from '../../injector/index.js';
4
10
  import { MailService } from '../../mail/mail.service.js';
5
11
  import { injectRepository } from '../../orm/server/index.js';
6
12
  import { setupIntegrationTest, truncateTables } from '../../unit-test/index.js';
7
13
  import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, WebPushSubscription } from '../models/index.js';
14
+ import { configureNotification } from '../server/module.js';
8
15
  import { EmailChannelProvider } from '../server/providers/email-channel-provider.js';
16
+ import { NotificationAncillaryService } from '../server/services/notification-ancillary.service.js';
9
17
  import { NotificationDeliveryWorker } from '../server/services/notification-delivery.worker.js';
10
18
  import { NotificationTypeService } from '../server/services/notification-type.service.js';
11
19
  import { NotificationService } from '../server/services/notification.service.js';
20
+ let MockNotificationAncillaryService = class MockNotificationAncillaryService extends NotificationAncillaryService {
21
+ async getViewData(notifications) {
22
+ return notifications.map(() => ({}));
23
+ }
24
+ };
25
+ MockNotificationAncillaryService = __decorate([
26
+ Singleton()
27
+ ], MockNotificationAncillaryService);
12
28
  describe('Notification Flow (Integration)', () => {
13
29
  let injector;
14
30
  let database;
@@ -34,6 +50,8 @@ describe('Notification Flow (Integration)', () => {
34
50
  // Mock MailService
35
51
  mailServiceMock = { send: vi.fn() };
36
52
  injector.register(MailService, { useValue: mailServiceMock });
53
+ // Mock NotificationAncillaryService
54
+ configureNotification({ injector, ancillaryService: MockNotificationAncillaryService });
37
55
  // Resolve Services
38
56
  notificationService = injector.resolve(NotificationService);
39
57
  worker = injector.resolve(NotificationDeliveryWorker);
@@ -65,14 +83,11 @@ describe('Notification Flow (Integration)', () => {
65
83
  escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
66
84
  },
67
85
  });
68
- const notification = Object.assign(new NotificationLogEntity(), {
69
- tenantId,
70
- userId: user.id,
86
+ await notificationService.send(tenantId, user.id, {
71
87
  type: 'test',
72
88
  priority: 'high',
73
89
  payload: { message: 'Hello', testField: 'Test Value' },
74
90
  });
75
- await notificationService.send(notification);
76
91
  const logs = await logRepo.loadManyByQuery({ tenantId });
77
92
  expect(logs).toHaveLength(1);
78
93
  const log = logs[0];
@@ -107,14 +122,12 @@ describe('Notification Flow (Integration)', () => {
107
122
  throttling: { limit: 1, interval: 60000 },
108
123
  },
109
124
  });
110
- const notification1 = Object.assign(new NotificationLogEntity(), {
111
- tenantId, userId: user.id, type: 'throttled', priority: 'medium', payload: {},
125
+ await notificationService.send(tenantId, user.id, {
126
+ type: 'throttled', priority: 'medium', payload: {},
112
127
  });
113
- const notification2 = Object.assign(new NotificationLogEntity(), {
114
- tenantId, userId: user.id, type: 'throttled', priority: 'medium', payload: {},
128
+ await notificationService.send(tenantId, user.id, {
129
+ type: 'throttled', priority: 'medium', payload: {},
115
130
  });
116
- await notificationService.send(notification1);
117
- await notificationService.send(notification2);
118
131
  const logs = await logRepo.loadManyByQuery({ tenantId });
119
132
  logs.sort((a, b) => Number(a.metadata.createTimestamp) - Number(b.metadata.createTimestamp));
120
133
  // First one should pass
@@ -135,10 +148,9 @@ describe('Notification Flow (Integration)', () => {
135
148
  escalations: [{ delay: 1000, channel: NotificationChannel.Email }],
136
149
  },
137
150
  });
138
- const notification = Object.assign(new NotificationLogEntity(), {
139
- tenantId, userId: user.id, type: 'readTest', priority: 'medium', payload: {},
151
+ await notificationService.send(tenantId, user.id, {
152
+ type: 'readTest', priority: 'medium', payload: {},
140
153
  });
141
- await notificationService.send(notification);
142
154
  const logs = await logRepo.loadManyByQuery({ tenantId });
143
155
  const log = logs[0];
144
156
  // Step 0: Deliver In-App
@@ -163,10 +175,9 @@ describe('Notification Flow (Integration)', () => {
163
175
  // Disable InApp, Enable Email (even though not default)
164
176
  await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.InApp, false);
165
177
  await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.Email, true);
166
- const notification = Object.assign(new NotificationLogEntity(), {
167
- tenantId, userId: user.id, type: 'prefTest', priority: 'medium', payload: {},
178
+ await notificationService.send(tenantId, user.id, {
179
+ type: 'prefTest', priority: 'medium', payload: {},
168
180
  });
169
- await notificationService.send(notification);
170
181
  const logs = await logRepo.loadManyByQuery({ tenantId });
171
182
  const log = logs[0];
172
183
  // Deliver
@@ -189,10 +200,9 @@ describe('Notification Flow (Integration)', () => {
189
200
  await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.WebPush, true);
190
201
  // Disable InApp so only WebPush is attempted
191
202
  await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.InApp, false);
192
- const notification = Object.assign(new NotificationLogEntity(), {
193
- tenantId, userId: user.id, type: 'unknownTest', priority: 'medium', payload: {},
203
+ await notificationService.send(tenantId, user.id, {
204
+ type: 'unknownTest', priority: 'medium', payload: {},
194
205
  });
195
- await notificationService.send(notification);
196
206
  const logs = await logRepo.loadManyByQuery({ tenantId });
197
207
  const log = logs[0];
198
208
  // Deliver
@@ -207,10 +217,9 @@ describe('Notification Flow (Integration)', () => {
207
217
  const inAppRepo = injectRepository(InAppNotification);
208
218
  const user = await subjectService.createUser({ tenantId, email: 'manage@example.com', firstName: 'Manage', lastName: 'User' });
209
219
  await typeService.initializeTypes({ manageTest: { label: 'Manage Test' } });
210
- const notification = Object.assign(new NotificationLogEntity(), {
211
- tenantId, userId: user.id, type: 'manageTest', priority: 'medium', payload: {},
220
+ await notificationService.send(tenantId, user.id, {
221
+ type: 'manageTest', priority: 'medium', payload: {},
212
222
  });
213
- await notificationService.send(notification);
214
223
  const logs = await logRepo.loadManyByQuery({ tenantId });
215
224
  await worker.deliver(logs[0].id);
216
225
  // List
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.104",
3
+ "version": "0.93.106",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -150,7 +150,7 @@
150
150
  "peerDependencies": {
151
151
  "@genkit-ai/google-genai": "^1.28",
152
152
  "@google-cloud/storage": "^7.18",
153
- "@google/genai": "^1.38",
153
+ "@google/genai": "^1.39",
154
154
  "@toon-format/toon": "^2.1.0",
155
155
  "@tstdl/angular": "^0.93",
156
156
  "@zxcvbn-ts/core": "^3.0",
@@ -164,12 +164,12 @@
164
164
  "minio": "^8.0",
165
165
  "mjml": "^4.18",
166
166
  "nodemailer": "^7.0",
167
- "pg": "^8.17",
167
+ "pg": "^8.18",
168
168
  "playwright": "^1.58",
169
169
  "preact": "^10.28",
170
170
  "preact-render-to-string": "^6.6",
171
171
  "sharp": "^0.34",
172
- "undici": "^7.19",
172
+ "undici": "^7.20",
173
173
  "urlpattern-polyfill": "^10.1",
174
174
  "zod": "^3.25"
175
175
  },
@@ -191,7 +191,7 @@
191
191
  "concurrently": "9.2",
192
192
  "drizzle-kit": "0.31",
193
193
  "eslint": "9.39",
194
- "globals": "17.2",
194
+ "globals": "17.3",
195
195
  "tsc-alias": "1.8",
196
196
  "typedoc-github-wiki-theme": "2.1",
197
197
  "typedoc-plugin-markdown": "4.9",
@@ -0,0 +1,2 @@
1
+ import type { Observable } from 'rxjs';
2
+ export declare function toAsyncIterable<T>(observable: Observable<T>): AsyncIterable<T>;
@@ -0,0 +1,15 @@
1
+ import { FeedableAsyncIterable } from '../utils/feedable-async-iterable.js';
2
+ export async function* toAsyncIterable(observable) {
3
+ const feed = new FeedableAsyncIterable();
4
+ const subscription = observable.subscribe({
5
+ next: (value) => feed.feed(value),
6
+ error: (err) => feed.throw(err),
7
+ complete: () => feed.end(),
8
+ });
9
+ try {
10
+ yield* feed;
11
+ }
12
+ finally {
13
+ subscription.unsubscribe();
14
+ }
15
+ }
@@ -1,3 +1,4 @@
1
+ export * from './async-iterable.js';
1
2
  export * from './cast.js';
2
3
  export * from './noop.js';
3
4
  export * from './reject-error.js';
@@ -1,3 +1,4 @@
1
+ export * from './async-iterable.js';
1
2
  export * from './cast.js';
2
3
  export * from './noop.js';
3
4
  export * from './reject-error.js';
package/test5.d.ts CHANGED
@@ -1 +1,13 @@
1
1
  import './polyfills.js';
2
+ import type { NotificationDefinitionMap } from './notification/index.js';
3
+ export type Notifications = NotificationDefinitionMap<{
4
+ 'analysis-started': {
5
+ payload: {
6
+ analysisId: string;
7
+ };
8
+ view: {
9
+ name: string;
10
+ };
11
+ };
12
+ 'analysis-completed': Record<never, never>;
13
+ }>;
package/test5.js CHANGED
@@ -3,18 +3,18 @@ import { Application } from './application/application.js';
3
3
  import { provideModule, provideSignalHandler } from './application/index.js';
4
4
  import { inject } from './injector/inject.js';
5
5
  import { JsonLogFormatter, PrettyPrintLogFormatter } from './logger/index.js';
6
- import { Logger } from './logger/logger.js';
7
6
  import { provideConsoleLogTransport } from './logger/transports/console.js';
7
+ import { NotificationService } from './notification/server/index.js';
8
8
  async function main(_cancellationSignal) {
9
- const logger = inject(Logger, 'Test');
10
- logger.error('Hello World!', { sessionId: 'abc' });
11
- logger.error('Hello World!', new Error('FOO!!'), { sessionId: 'def' });
12
- logger.error(new Error('BAR!!'), { sessionId: 'ghi' });
13
- // logger.warn('Hello World!');
14
- // logger.info('Hello World!');
15
- // logger.verbose('Hello World!');
16
- // logger.debug('Hello World!');
17
- // logger.trace('Hello World!');
9
+ const notificationService = inject((NotificationService));
10
+ const notificationApi = new null();
11
+ const notification$ = (await notificationApi.stream());
12
+ const tenantId = 'test-tenant';
13
+ const userId = 'user-123';
14
+ await notificationService.send(tenantId, userId, {
15
+ type: 'analysis-started',
16
+ payload: { analysisId: 'analysis-456' },
17
+ });
18
18
  }
19
19
  Application.run('Test', [
20
20
  provideConsoleLogTransport(PrettyPrintLogFormatter),