@tstdl/base 0.93.106 → 0.93.108
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/data-structures/collection.d.ts +1 -1
- package/data-structures/collection.js +2 -1
- package/notification/api/notification.api.d.ts +81 -1
- package/notification/api/notification.api.js +15 -0
- package/notification/models/in-app-notification.model.js +1 -1
- package/notification/models/notification-log.model.d.ts +6 -7
- package/notification/models/notification-log.model.js +17 -12
- package/notification/server/api/index.d.ts +1 -0
- package/notification/server/api/index.js +1 -0
- package/notification/server/drizzle/{0000_ancient_hellion.sql → 0000_oval_rage.sql} +5 -6
- package/notification/server/drizzle/meta/0000_snapshot.json +62 -42
- package/notification/server/drizzle/meta/_journal.json +2 -2
- package/notification/server/index.d.ts +1 -0
- package/notification/server/index.js +1 -0
- package/notification/server/services/notification-delivery.worker.js +2 -2
- package/notification/server/services/notification.service.d.ts +1 -1
- package/notification/server/services/notification.service.js +6 -4
- package/notification/tests/notification-flow.test.js +8 -7
- package/package.json +4 -2
- package/task-queue/postgres/task-queue.d.ts +1 -0
- package/task-queue/postgres/task-queue.js +4 -1
- package/task-queue/task-context.d.ts +1 -0
- package/task-queue/task-context.js +3 -0
- package/task-queue/task-queue.d.ts +7 -0
- package/task-queue/tests/worker.test.js +29 -0
- package/test5.js +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Observable } from 'rxjs';
|
|
2
2
|
import type { ToJson } from '../interfaces.js';
|
|
3
|
-
import { type Signal } from '../signals/
|
|
3
|
+
import { type Signal } from '../signals/api.js';
|
|
4
4
|
export declare abstract class Collection<T, TItemsIterator extends IterableIterator<any>, TThis extends Collection<T, TItemsIterator, TThis> = Collection<T, TItemsIterator, any>> implements Iterable<T>, ToJson {
|
|
5
5
|
private readonly sizeSubject;
|
|
6
6
|
private readonly changeSubject;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, Subject, distinctUntilChanged, filter, firstValueFrom, map, startWith } from 'rxjs';
|
|
2
|
-
import {
|
|
2
|
+
import { untracked } from '../signals/api.js';
|
|
3
|
+
import { toLazySignal } from '../signals/to-lazy-signal.js';
|
|
3
4
|
import { lazyProperty } from '../utils/object/lazy-property.js';
|
|
4
5
|
export class Collection {
|
|
5
6
|
sizeSubject = new BehaviorSubject(0);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataStream } from '../../sse/data-stream.js';
|
|
2
2
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
3
|
+
export type NotificationApiDefinition = typeof notificationApiDefinition;
|
|
3
4
|
export declare const notificationApiDefinition: {
|
|
4
5
|
resource: string;
|
|
5
6
|
endpoints: {
|
|
@@ -77,4 +78,83 @@ export declare const notificationApiDefinition: {
|
|
|
77
78
|
};
|
|
78
79
|
};
|
|
79
80
|
};
|
|
80
|
-
|
|
81
|
+
declare const _NotificationApiClient: import("../../api/client/index.js").ApiClient<{
|
|
82
|
+
resource: string;
|
|
83
|
+
endpoints: {
|
|
84
|
+
stream: {
|
|
85
|
+
resource: string;
|
|
86
|
+
method: "GET";
|
|
87
|
+
result: {
|
|
88
|
+
new (): DataStream<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
|
|
89
|
+
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
90
|
+
};
|
|
91
|
+
credentials: true;
|
|
92
|
+
};
|
|
93
|
+
listInApp: {
|
|
94
|
+
resource: string;
|
|
95
|
+
method: "GET";
|
|
96
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
97
|
+
offset?: number | undefined;
|
|
98
|
+
limit?: number | undefined;
|
|
99
|
+
includeArchived?: boolean | undefined;
|
|
100
|
+
}>;
|
|
101
|
+
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>>>>;
|
|
102
|
+
credentials: true;
|
|
103
|
+
};
|
|
104
|
+
markRead: {
|
|
105
|
+
resource: string;
|
|
106
|
+
method: "POST";
|
|
107
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
108
|
+
id: string;
|
|
109
|
+
}>;
|
|
110
|
+
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
111
|
+
credentials: true;
|
|
112
|
+
};
|
|
113
|
+
archive: {
|
|
114
|
+
resource: string;
|
|
115
|
+
method: "POST";
|
|
116
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
117
|
+
id: string;
|
|
118
|
+
}>;
|
|
119
|
+
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
120
|
+
credentials: true;
|
|
121
|
+
};
|
|
122
|
+
getPreferences: {
|
|
123
|
+
resource: string;
|
|
124
|
+
method: "GET";
|
|
125
|
+
result: import("../../schema/index.js").ArraySchema<{
|
|
126
|
+
type: string;
|
|
127
|
+
enabled: boolean;
|
|
128
|
+
channel: "email" | "in-app" | "web-push";
|
|
129
|
+
}>;
|
|
130
|
+
credentials: true;
|
|
131
|
+
};
|
|
132
|
+
updatePreference: {
|
|
133
|
+
resource: string;
|
|
134
|
+
method: "POST";
|
|
135
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
136
|
+
readonly type: string;
|
|
137
|
+
readonly channel: "email" | "in-app" | "web-push";
|
|
138
|
+
readonly enabled: boolean;
|
|
139
|
+
}>;
|
|
140
|
+
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
141
|
+
credentials: true;
|
|
142
|
+
};
|
|
143
|
+
registerWebPush: {
|
|
144
|
+
resource: string;
|
|
145
|
+
method: "POST";
|
|
146
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
147
|
+
readonly endpoint: string;
|
|
148
|
+
readonly keys: {
|
|
149
|
+
p256dhBase64: string;
|
|
150
|
+
authBase64: string;
|
|
151
|
+
};
|
|
152
|
+
}>;
|
|
153
|
+
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
154
|
+
credentials: true;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
}>;
|
|
158
|
+
export declare class NotificationApiClient extends _NotificationApiClient {
|
|
159
|
+
}
|
|
160
|
+
export {};
|
|
@@ -1,4 +1,12 @@
|
|
|
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 { compileClient } from '../../api/client/index.js';
|
|
1
8
|
import { defineApi } from '../../api/types.js';
|
|
9
|
+
import { ReplaceClass } from '../../injector/decorators.js';
|
|
2
10
|
import { array, boolean, enumeration, explicitObject, literal, number, object, optional, string } from '../../schema/index.js';
|
|
3
11
|
import { DataStream } from '../../sse/data-stream.js';
|
|
4
12
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
@@ -73,3 +81,10 @@ export const notificationApiDefinition = defineApi({
|
|
|
73
81
|
},
|
|
74
82
|
},
|
|
75
83
|
});
|
|
84
|
+
const _NotificationApiClient = compileClient(notificationApiDefinition);
|
|
85
|
+
let NotificationApiClient = class NotificationApiClient extends _NotificationApiClient {
|
|
86
|
+
};
|
|
87
|
+
NotificationApiClient = __decorate([
|
|
88
|
+
ReplaceClass(_NotificationApiClient)
|
|
89
|
+
], NotificationApiClient);
|
|
90
|
+
export { NotificationApiClient };
|
|
@@ -40,7 +40,7 @@ __decorate([
|
|
|
40
40
|
InAppNotification = __decorate([
|
|
41
41
|
NotificationTable({ name: 'in_app' }),
|
|
42
42
|
ForeignKey(() => NotificationLogEntity, ['tenantId', 'logId', 'userId'], ['tenantId', 'id', 'userId']),
|
|
43
|
-
Index(['tenantId', 'userId', '
|
|
43
|
+
Index(['tenantId', 'userId', 'logId'], { where: () => ({ archiveTimestamp: null }) })
|
|
44
44
|
], InAppNotification);
|
|
45
45
|
export { InAppNotification };
|
|
46
46
|
export class InAppNotificationView extends InAppNotification {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { Merge } from 'type-fest';
|
|
1
2
|
import { type EnumType } from '../../enumeration/index.js';
|
|
2
|
-
import { type Json,
|
|
3
|
+
import { type Json, TenantBaseEntity, type Timestamp, type Uuid } from '../../orm/index.js';
|
|
3
4
|
import type { ObjectLiteral } from '../../types/types.js';
|
|
4
5
|
export declare const NotificationChannel: {
|
|
5
6
|
readonly InApp: "in-app";
|
|
@@ -44,19 +45,17 @@ export type NotificationLog<Definitions extends NotificationDefinitionMap = Noti
|
|
|
44
45
|
export type NotificationLogView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
45
46
|
[Type in NotificationTypes<Definitions>]: Omit<NotificationLogEntity<Definitions, Type>, 'type' | 'payload'> & {
|
|
46
47
|
type: Type;
|
|
47
|
-
payload: NotificationPayload<Definitions, Type
|
|
48
|
-
view: NotificationView<Definitions, Type>;
|
|
48
|
+
payload: Merge<NotificationPayload<Definitions, Type>, NotificationView<Definitions, Type>>;
|
|
49
49
|
};
|
|
50
50
|
}[NotificationTypes<Definitions>];
|
|
51
|
-
export declare class NotificationLogEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends
|
|
51
|
+
export declare class NotificationLogEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends TenantBaseEntity {
|
|
52
52
|
static readonly entityName = "NotificationLog";
|
|
53
53
|
userId: Uuid;
|
|
54
54
|
type: Type;
|
|
55
|
+
timestamp: Timestamp;
|
|
55
56
|
priority: NotificationPriority;
|
|
56
57
|
status: NotificationStatus;
|
|
57
58
|
currentStep: number;
|
|
59
|
+
triggerSubjectId: Uuid;
|
|
58
60
|
payload: Json<NotificationPayload<Definitions, Type>> | null;
|
|
59
61
|
}
|
|
60
|
-
export declare class NotificationLogViewEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends NotificationLogEntity<Definitions, Type> {
|
|
61
|
-
view?: unknown;
|
|
62
|
-
}
|
|
@@ -7,10 +7,10 @@ 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 { User } from '../../authentication/
|
|
10
|
+
import { Subject, User } from '../../authentication/index.js';
|
|
11
11
|
import { defineEnum } from '../../enumeration/index.js';
|
|
12
|
-
import { JsonProperty, Reference,
|
|
13
|
-
import { Enumeration, Integer, StringProperty
|
|
12
|
+
import { Index, JsonProperty, Reference, TenantBaseEntity, TenantReference, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
|
|
13
|
+
import { Enumeration, Integer, StringProperty } 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', {
|
|
@@ -31,13 +31,15 @@ export const NotificationStatus = defineEnum('NotificationStatus', {
|
|
|
31
31
|
Read: 'read',
|
|
32
32
|
Failed: 'failed',
|
|
33
33
|
});
|
|
34
|
-
let NotificationLogEntity = class NotificationLogEntity extends
|
|
34
|
+
let NotificationLogEntity = class NotificationLogEntity extends TenantBaseEntity {
|
|
35
35
|
static entityName = 'NotificationLog';
|
|
36
36
|
userId;
|
|
37
37
|
type;
|
|
38
|
+
timestamp;
|
|
38
39
|
priority;
|
|
39
40
|
status;
|
|
40
41
|
currentStep;
|
|
42
|
+
triggerSubjectId;
|
|
41
43
|
payload;
|
|
42
44
|
};
|
|
43
45
|
__decorate([
|
|
@@ -50,6 +52,10 @@ __decorate([
|
|
|
50
52
|
StringProperty(),
|
|
51
53
|
__metadata("design:type", String)
|
|
52
54
|
], NotificationLogEntity.prototype, "type", void 0);
|
|
55
|
+
__decorate([
|
|
56
|
+
TimestampProperty(),
|
|
57
|
+
__metadata("design:type", Number)
|
|
58
|
+
], NotificationLogEntity.prototype, "timestamp", void 0);
|
|
53
59
|
__decorate([
|
|
54
60
|
Enumeration(NotificationPriority),
|
|
55
61
|
__metadata("design:type", String)
|
|
@@ -62,19 +68,18 @@ __decorate([
|
|
|
62
68
|
Integer(),
|
|
63
69
|
__metadata("design:type", Number)
|
|
64
70
|
], NotificationLogEntity.prototype, "currentStep", void 0);
|
|
71
|
+
__decorate([
|
|
72
|
+
TenantReference(() => Subject),
|
|
73
|
+
UuidProperty(),
|
|
74
|
+
__metadata("design:type", String)
|
|
75
|
+
], NotificationLogEntity.prototype, "triggerSubjectId", void 0);
|
|
65
76
|
__decorate([
|
|
66
77
|
JsonProperty({ nullable: true }),
|
|
67
78
|
__metadata("design:type", Object)
|
|
68
79
|
], NotificationLogEntity.prototype, "payload", void 0);
|
|
69
80
|
NotificationLogEntity = __decorate([
|
|
70
81
|
NotificationTable({ name: 'log' }),
|
|
71
|
-
Unique(['tenantId', 'id', 'userId'])
|
|
82
|
+
Unique(['tenantId', 'id', 'userId']),
|
|
83
|
+
Index(['tenantId', 'userId', 'timestamp'])
|
|
72
84
|
], NotificationLogEntity);
|
|
73
85
|
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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification.api-controller.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification.api-controller.js';
|
|
@@ -16,15 +16,12 @@ CREATE TABLE "notification"."log" (
|
|
|
16
16
|
"tenant_id" uuid NOT NULL,
|
|
17
17
|
"user_id" uuid NOT NULL,
|
|
18
18
|
"type" text NOT NULL,
|
|
19
|
+
"timestamp" timestamp with time zone NOT NULL,
|
|
19
20
|
"priority" "notification"."notification_priority" NOT NULL,
|
|
20
21
|
"status" "notification"."notification_status" NOT NULL,
|
|
21
22
|
"current_step" integer NOT NULL,
|
|
23
|
+
"trigger_subject_id" uuid NOT NULL,
|
|
22
24
|
"payload" jsonb,
|
|
23
|
-
"revision" integer NOT NULL,
|
|
24
|
-
"revision_timestamp" timestamp with time zone NOT NULL,
|
|
25
|
-
"create_timestamp" timestamp with time zone NOT NULL,
|
|
26
|
-
"delete_timestamp" timestamp with time zone,
|
|
27
|
-
"attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
28
25
|
CONSTRAINT "log_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
|
|
29
26
|
CONSTRAINT "log_tenant_id_id_user_id_unique" UNIQUE("tenant_id","id","user_id")
|
|
30
27
|
);
|
|
@@ -79,7 +76,9 @@ ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_id_user_fkey" FOREIGN
|
|
|
79
76
|
ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_tenantId_logId_userId_log_fkey" FOREIGN KEY ("tenant_id","log_id","user_id") REFERENCES "notification"."log"("tenant_id","id","user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
80
77
|
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
81
78
|
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
79
|
+
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_subject_fkey" FOREIGN KEY ("tenant_id","trigger_subject_id") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
82
80
|
ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
83
81
|
ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
84
82
|
ALTER TABLE "notification"."web_push_subscription" ADD CONSTRAINT "web_push_subscription_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
85
|
-
CREATE INDEX "
|
|
83
|
+
CREATE INDEX "in_app_tenant_id_user_id_log_id_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","log_id") WHERE "notification"."in_app"."archive_timestamp" is null;--> statement-breakpoint
|
|
84
|
+
CREATE INDEX "log_tenant_id_user_id_timestamp_idx" ON "notification"."log" USING btree ("tenant_id","user_id","timestamp");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "90cd6144-d455-4a13-8148-ca119f8bfe79",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"indexes": {
|
|
50
|
-
"
|
|
51
|
-
"name": "
|
|
50
|
+
"in_app_tenant_id_user_id_log_id_idx": {
|
|
51
|
+
"name": "in_app_tenant_id_user_id_log_id_idx",
|
|
52
52
|
"columns": [
|
|
53
53
|
{
|
|
54
54
|
"expression": "tenant_id",
|
|
@@ -63,19 +63,14 @@
|
|
|
63
63
|
"nulls": "last"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
"expression": "
|
|
67
|
-
"isExpression": false,
|
|
68
|
-
"asc": true,
|
|
69
|
-
"nulls": "last"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"expression": "archive_timestamp",
|
|
66
|
+
"expression": "log_id",
|
|
73
67
|
"isExpression": false,
|
|
74
68
|
"asc": true,
|
|
75
69
|
"nulls": "last"
|
|
76
70
|
}
|
|
77
71
|
],
|
|
78
72
|
"isUnique": false,
|
|
73
|
+
"where": "\"notification\".\"in_app\".\"archive_timestamp\" is null",
|
|
79
74
|
"concurrently": false,
|
|
80
75
|
"method": "btree",
|
|
81
76
|
"with": {}
|
|
@@ -160,6 +155,12 @@
|
|
|
160
155
|
"primaryKey": false,
|
|
161
156
|
"notNull": true
|
|
162
157
|
},
|
|
158
|
+
"timestamp": {
|
|
159
|
+
"name": "timestamp",
|
|
160
|
+
"type": "timestamp with time zone",
|
|
161
|
+
"primaryKey": false,
|
|
162
|
+
"notNull": true
|
|
163
|
+
},
|
|
163
164
|
"priority": {
|
|
164
165
|
"name": "priority",
|
|
165
166
|
"type": "notification_priority",
|
|
@@ -180,45 +181,48 @@
|
|
|
180
181
|
"primaryKey": false,
|
|
181
182
|
"notNull": true
|
|
182
183
|
},
|
|
183
|
-
"
|
|
184
|
-
"name": "
|
|
185
|
-
"type": "
|
|
186
|
-
"primaryKey": false,
|
|
187
|
-
"notNull": false
|
|
188
|
-
},
|
|
189
|
-
"revision": {
|
|
190
|
-
"name": "revision",
|
|
191
|
-
"type": "integer",
|
|
192
|
-
"primaryKey": false,
|
|
193
|
-
"notNull": true
|
|
194
|
-
},
|
|
195
|
-
"revision_timestamp": {
|
|
196
|
-
"name": "revision_timestamp",
|
|
197
|
-
"type": "timestamp with time zone",
|
|
198
|
-
"primaryKey": false,
|
|
199
|
-
"notNull": true
|
|
200
|
-
},
|
|
201
|
-
"create_timestamp": {
|
|
202
|
-
"name": "create_timestamp",
|
|
203
|
-
"type": "timestamp with time zone",
|
|
184
|
+
"trigger_subject_id": {
|
|
185
|
+
"name": "trigger_subject_id",
|
|
186
|
+
"type": "uuid",
|
|
204
187
|
"primaryKey": false,
|
|
205
188
|
"notNull": true
|
|
206
189
|
},
|
|
207
|
-
"
|
|
208
|
-
"name": "
|
|
209
|
-
"type": "timestamp with time zone",
|
|
210
|
-
"primaryKey": false,
|
|
211
|
-
"notNull": false
|
|
212
|
-
},
|
|
213
|
-
"attributes": {
|
|
214
|
-
"name": "attributes",
|
|
190
|
+
"payload": {
|
|
191
|
+
"name": "payload",
|
|
215
192
|
"type": "jsonb",
|
|
216
193
|
"primaryKey": false,
|
|
217
|
-
"notNull":
|
|
218
|
-
|
|
194
|
+
"notNull": false
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"indexes": {
|
|
198
|
+
"log_tenant_id_user_id_timestamp_idx": {
|
|
199
|
+
"name": "log_tenant_id_user_id_timestamp_idx",
|
|
200
|
+
"columns": [
|
|
201
|
+
{
|
|
202
|
+
"expression": "tenant_id",
|
|
203
|
+
"isExpression": false,
|
|
204
|
+
"asc": true,
|
|
205
|
+
"nulls": "last"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"expression": "user_id",
|
|
209
|
+
"isExpression": false,
|
|
210
|
+
"asc": true,
|
|
211
|
+
"nulls": "last"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"expression": "timestamp",
|
|
215
|
+
"isExpression": false,
|
|
216
|
+
"asc": true,
|
|
217
|
+
"nulls": "last"
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"isUnique": false,
|
|
221
|
+
"concurrently": false,
|
|
222
|
+
"method": "btree",
|
|
223
|
+
"with": {}
|
|
219
224
|
}
|
|
220
225
|
},
|
|
221
|
-
"indexes": {},
|
|
222
226
|
"foreignKeys": {
|
|
223
227
|
"log_type_type_key_fk": {
|
|
224
228
|
"name": "log_type_type_key_fk",
|
|
@@ -249,6 +253,22 @@
|
|
|
249
253
|
],
|
|
250
254
|
"onDelete": "no action",
|
|
251
255
|
"onUpdate": "no action"
|
|
256
|
+
},
|
|
257
|
+
"log_id_subject_fkey": {
|
|
258
|
+
"name": "log_id_subject_fkey",
|
|
259
|
+
"tableFrom": "log",
|
|
260
|
+
"tableTo": "subject",
|
|
261
|
+
"schemaTo": "authentication",
|
|
262
|
+
"columnsFrom": [
|
|
263
|
+
"tenant_id",
|
|
264
|
+
"trigger_subject_id"
|
|
265
|
+
],
|
|
266
|
+
"columnsTo": [
|
|
267
|
+
"tenant_id",
|
|
268
|
+
"id"
|
|
269
|
+
],
|
|
270
|
+
"onDelete": "no action",
|
|
271
|
+
"onUpdate": "no action"
|
|
252
272
|
}
|
|
253
273
|
},
|
|
254
274
|
"compositePrimaryKeys": {
|
|
@@ -80,7 +80,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
|
|
|
80
80
|
for (const channel of enabledChannels) {
|
|
81
81
|
// TODO: what if an error occurs here? partial delivery? Should we use tasks to allow retrying individual channels?
|
|
82
82
|
const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
|
|
83
|
-
await this.sendToChannel({ ...notification,
|
|
83
|
+
await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, channel, tx);
|
|
84
84
|
}
|
|
85
85
|
await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { status: NotificationStatus.Sent, currentStep: 1 });
|
|
86
86
|
if (isNotNull(type.escalations) && (type.escalations.length > 0)) {
|
|
@@ -99,7 +99,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
|
|
|
99
99
|
return TaskProcessResult.Complete();
|
|
100
100
|
}
|
|
101
101
|
const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
|
|
102
|
-
await this.sendToChannel({ ...notification,
|
|
102
|
+
await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, rule.channel, tx);
|
|
103
103
|
await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { currentStep: step + 1 });
|
|
104
104
|
if (step < type.escalations.length) {
|
|
105
105
|
const nextRule = type.escalations[step];
|
|
@@ -4,7 +4,7 @@ import type { TypedOmit } from '../../../types/types.js';
|
|
|
4
4
|
import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationDefinitionMap, type NotificationLog } from '../../models/index.js';
|
|
5
5
|
export declare class NotificationService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
|
|
6
6
|
#private;
|
|
7
|
-
send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | '
|
|
7
|
+
send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'timestamp' | 'status' | 'currentStep' | 'priority'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>): Promise<void>;
|
|
8
8
|
listInApp(tenantId: string, userId: string, options?: {
|
|
9
9
|
limit?: number;
|
|
10
10
|
offset?: number;
|
|
@@ -25,6 +25,7 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
25
25
|
const notificationToInsert = {
|
|
26
26
|
tenantId,
|
|
27
27
|
userId,
|
|
28
|
+
timestamp: TRANSACTION_TIMESTAMP,
|
|
28
29
|
priority: NotificationPriority.Medium,
|
|
29
30
|
...notification,
|
|
30
31
|
status: NotificationStatus.Pending,
|
|
@@ -41,11 +42,11 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
41
42
|
inApp: inAppNotification,
|
|
42
43
|
})
|
|
43
44
|
.from(notificationLog)
|
|
44
|
-
.innerJoin(inAppNotification, eq(inAppNotification.logId, notificationLog.id))
|
|
45
|
-
.where(and(eq(
|
|
45
|
+
.innerJoin(inAppNotification, and(eq(inAppNotification.tenantId, notificationLog.tenantId), eq(inAppNotification.userId, notificationLog.userId), eq(inAppNotification.logId, notificationLog.id)))
|
|
46
|
+
.where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp)))
|
|
46
47
|
.limit(options.limit ?? 50)
|
|
47
48
|
.offset(options.offset ?? 0)
|
|
48
|
-
.orderBy(desc(notificationLog.
|
|
49
|
+
.orderBy(desc(notificationLog.timestamp));
|
|
49
50
|
const inAppRows = rows.map((row) => row.inApp);
|
|
50
51
|
const notificationRows = rows.map((row) => row.notification);
|
|
51
52
|
const notificationEntities = await this.#notificationLogRepository.mapManyToEntity(notificationRows);
|
|
@@ -54,7 +55,8 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
54
55
|
const views = notificationEntities.map((notification, index) => {
|
|
55
56
|
const inApp = inAppEntities[index];
|
|
56
57
|
const viewData = notificationViewDatas[index];
|
|
57
|
-
|
|
58
|
+
const payload = { ...notification.payload, ...viewData };
|
|
59
|
+
return toInAppNotificationView(inApp, { ...notification, payload });
|
|
58
60
|
});
|
|
59
61
|
return views;
|
|
60
62
|
}
|
|
@@ -86,6 +86,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
86
86
|
await notificationService.send(tenantId, user.id, {
|
|
87
87
|
type: 'test',
|
|
88
88
|
priority: 'high',
|
|
89
|
+
triggerSubjectId: user.id,
|
|
89
90
|
payload: { message: 'Hello', testField: 'Test Value' },
|
|
90
91
|
});
|
|
91
92
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
@@ -123,13 +124,13 @@ describe('Notification Flow (Integration)', () => {
|
|
|
123
124
|
},
|
|
124
125
|
});
|
|
125
126
|
await notificationService.send(tenantId, user.id, {
|
|
126
|
-
type: 'throttled', priority: 'medium', payload: {},
|
|
127
|
+
type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
127
128
|
});
|
|
128
129
|
await notificationService.send(tenantId, user.id, {
|
|
129
|
-
type: 'throttled', priority: 'medium', payload: {},
|
|
130
|
+
type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
130
131
|
});
|
|
131
132
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
132
|
-
logs.sort((a, b) => Number(a.
|
|
133
|
+
logs.sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
|
|
133
134
|
// First one should pass
|
|
134
135
|
const result1 = await worker.deliver(logs[0].id);
|
|
135
136
|
expect(result1.payload.action).toBe('complete'); // No escalations
|
|
@@ -149,7 +150,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
149
150
|
},
|
|
150
151
|
});
|
|
151
152
|
await notificationService.send(tenantId, user.id, {
|
|
152
|
-
type: 'readTest', priority: 'medium', payload: {},
|
|
153
|
+
type: 'readTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
153
154
|
});
|
|
154
155
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
155
156
|
const log = logs[0];
|
|
@@ -176,7 +177,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
176
177
|
await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.InApp, false);
|
|
177
178
|
await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.Email, true);
|
|
178
179
|
await notificationService.send(tenantId, user.id, {
|
|
179
|
-
type: 'prefTest', priority: 'medium', payload: {},
|
|
180
|
+
type: 'prefTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
180
181
|
});
|
|
181
182
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
182
183
|
const log = logs[0];
|
|
@@ -201,7 +202,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
201
202
|
// Disable InApp so only WebPush is attempted
|
|
202
203
|
await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.InApp, false);
|
|
203
204
|
await notificationService.send(tenantId, user.id, {
|
|
204
|
-
type: 'unknownTest', priority: 'medium', payload: {},
|
|
205
|
+
type: 'unknownTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
205
206
|
});
|
|
206
207
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
207
208
|
const log = logs[0];
|
|
@@ -218,7 +219,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
218
219
|
const user = await subjectService.createUser({ tenantId, email: 'manage@example.com', firstName: 'Manage', lastName: 'User' });
|
|
219
220
|
await typeService.initializeTypes({ manageTest: { label: 'Manage Test' } });
|
|
220
221
|
await notificationService.send(tenantId, user.id, {
|
|
221
|
-
type: 'manageTest', priority: 'medium', payload: {},
|
|
222
|
+
type: 'manageTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
|
|
222
223
|
});
|
|
223
224
|
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
224
225
|
await worker.deliver(logs[0].id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.108",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
"./message-bus": "./message-bus/index.js",
|
|
87
87
|
"./migration": "./migration/index.js",
|
|
88
88
|
"./module": "./module/index.js",
|
|
89
|
+
"./notification": "./notification/index.js",
|
|
90
|
+
"./notification/server": "./notification/server/index.js",
|
|
89
91
|
"./object-storage": "./object-storage/index.js",
|
|
90
92
|
"./object-storage/google": "./object-storage/google/index.js",
|
|
91
93
|
"./object-storage/s3": "./object-storage/s3/index.js",
|
|
@@ -163,7 +165,7 @@
|
|
|
163
165
|
"handlebars": "^4.7",
|
|
164
166
|
"minio": "^8.0",
|
|
165
167
|
"mjml": "^4.18",
|
|
166
|
-
"nodemailer": "^
|
|
168
|
+
"nodemailer": "^8.0",
|
|
167
169
|
"pg": "^8.18",
|
|
168
170
|
"playwright": "^1.58",
|
|
169
171
|
"preact": "^10.28",
|
|
@@ -116,6 +116,7 @@ export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = T
|
|
|
116
116
|
resetState?: boolean;
|
|
117
117
|
transaction?: Transaction;
|
|
118
118
|
}): Promise<void>;
|
|
119
|
+
notify(): void;
|
|
119
120
|
getConsumer<Type extends TaskTypes<Definitions>>(cancellationSignal: CancellationSignal, options?: {
|
|
120
121
|
forceDequeue?: boolean;
|
|
121
122
|
types?: Type[];
|
|
@@ -60,6 +60,7 @@ import { and, asc, count, eq, gt, gte, inArray, lt, lte, notInArray, or, sql, is
|
|
|
60
60
|
import { merge } from 'rxjs';
|
|
61
61
|
import { CancellationSignal } from '../../cancellation/index.js';
|
|
62
62
|
import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
|
|
63
|
+
import { serializeError } from '../../errors/index.js';
|
|
63
64
|
import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
|
|
64
65
|
import { Logger } from '../../logger/index.js';
|
|
65
66
|
import { MessageBus } from '../../message-bus/index.js';
|
|
@@ -69,7 +70,6 @@ import { RateLimiter } from '../../rate-limit/index.js';
|
|
|
69
70
|
import { createArray, distinct, toArray } from '../../utils/array/array.js';
|
|
70
71
|
import { digest } from '../../utils/cryptography.js';
|
|
71
72
|
import { currentTimestamp } from '../../utils/date-time.js';
|
|
72
|
-
import { serializeError } from '../../errors/index.js';
|
|
73
73
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
74
74
|
import { isDefined, isNotNull, isNull, isString, isUndefined } from '../../utils/type-guards.js';
|
|
75
75
|
import { millisecondsPerSecond } from '../../utils/units.js';
|
|
@@ -987,6 +987,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
987
987
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))))
|
|
988
988
|
.execute();
|
|
989
989
|
}
|
|
990
|
+
notify() {
|
|
991
|
+
this.#messageBus.publishAndForget();
|
|
992
|
+
}
|
|
990
993
|
async *getConsumer(cancellationSignal, options) {
|
|
991
994
|
const continue$ = merge(this.#messageBus.allMessages$, cancellationSignal);
|
|
992
995
|
while (cancellationSignal.isUnset) {
|
|
@@ -16,6 +16,7 @@ export declare class TaskContext<Definitions extends TaskDefinitionMap, Type ext
|
|
|
16
16
|
get state(): TaskState<Definitions, Type> | null;
|
|
17
17
|
get attempt(): number;
|
|
18
18
|
get triesLeft(): number;
|
|
19
|
+
get isFinalAttempt(): boolean;
|
|
19
20
|
get signal(): CancellationSignal;
|
|
20
21
|
get logger(): Logger;
|
|
21
22
|
complete(result?: TaskResult<Definitions, Type>, options?: {
|
|
@@ -285,6 +285,13 @@ export declare abstract class TaskQueue<Definitions extends TaskDefinitionMap =
|
|
|
285
285
|
resetState?: boolean;
|
|
286
286
|
transaction?: Transaction;
|
|
287
287
|
}): Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* Notifies the queue that new tasks might be available for processing.
|
|
290
|
+
* Used to wake up waiting consumers.
|
|
291
|
+
* Useful for unit tests.
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
abstract notify(): void;
|
|
288
295
|
abstract getConsumer<Type extends TaskTypes<Definitions>>(cancellationSignal: CancellationSignal, options?: {
|
|
289
296
|
forceDequeue?: boolean;
|
|
290
297
|
types?: Type[];
|
|
@@ -139,4 +139,33 @@ describe('Worker & Base Class Tests', () => {
|
|
|
139
139
|
token.set();
|
|
140
140
|
expect(executed).toBe(true);
|
|
141
141
|
});
|
|
142
|
+
it('should correctly report isFinalAttempt in TaskContext', async () => {
|
|
143
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
144
|
+
const queueName = `final-try-test-${Date.now()}`;
|
|
145
|
+
const testQueue = queueProvider.get(queueName, {
|
|
146
|
+
maxTries: 2,
|
|
147
|
+
retryDelayMinimum: 0,
|
|
148
|
+
retryDelayGrowth: 1,
|
|
149
|
+
});
|
|
150
|
+
await testQueue.enqueue('work', {});
|
|
151
|
+
testQueue.notify();
|
|
152
|
+
const token = new CancellationToken();
|
|
153
|
+
const finalAttemptValues = [];
|
|
154
|
+
testQueue.process({ cancellationSignal: token }, async (context) => {
|
|
155
|
+
finalAttemptValues.push(context.isFinalAttempt);
|
|
156
|
+
if (context.attempt === 1) {
|
|
157
|
+
throw new Error('fail first attempt');
|
|
158
|
+
}
|
|
159
|
+
return TaskProcessResult.Complete();
|
|
160
|
+
});
|
|
161
|
+
for (let i = 0; i < 100; i++) {
|
|
162
|
+
if (finalAttemptValues.length === 2)
|
|
163
|
+
break;
|
|
164
|
+
testQueue.notify();
|
|
165
|
+
await timeout(100);
|
|
166
|
+
}
|
|
167
|
+
token.set();
|
|
168
|
+
expect(finalAttemptValues).toEqual([false, true]);
|
|
169
|
+
await testQueue.clear();
|
|
170
|
+
});
|
|
142
171
|
});
|
package/test5.js
CHANGED