unified-notification-core 0.1.0
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/core.d.ts +72 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +617 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +46 -0
- package/dist/preferences.js.map +1 -0
- package/dist/schema.d.ts +2237 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +149 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/docs/adoption-notes.md +45 -0
- package/docs/architecture.md +75 -0
- package/examples/basic.ts +74 -0
- package/package.json +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAaF,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuKxC;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6B,CAAC;AAE7D,eAAO,MACL,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IACb,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IACtB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EACH,CAAC;AAEvB,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { boolean, check, index, integer, jsonb, pgTableCreator, primaryKey, text, timestamp, uniqueIndex, uuid, varchar, } from "drizzle-orm/pg-core";
|
|
3
|
+
function validatePrefix(prefix) {
|
|
4
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(prefix)) {
|
|
5
|
+
throw new Error("tablePrefix must start with a letter or underscore and contain only letters, numbers, and underscores");
|
|
6
|
+
}
|
|
7
|
+
if (prefix.length > 23) {
|
|
8
|
+
throw new Error("tablePrefix must be at most 23 characters");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function createNotificationSchema(options = {}) {
|
|
12
|
+
const prefix = options.tablePrefix ?? "unc_";
|
|
13
|
+
validatePrefix(prefix);
|
|
14
|
+
const table = pgTableCreator((name) => `${prefix}${name}`);
|
|
15
|
+
const name = (suffix) => `${prefix}${suffix}`;
|
|
16
|
+
const notifications = table("notifications", {
|
|
17
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
18
|
+
recipientId: text("recipient_id").notNull(),
|
|
19
|
+
topic: varchar("topic", { length: 191 }).notNull(),
|
|
20
|
+
title: text("title").notNull(),
|
|
21
|
+
body: text("body").notNull(),
|
|
22
|
+
actionUrl: text("action_url"),
|
|
23
|
+
data: jsonb("data").$type().notNull().default({}),
|
|
24
|
+
payload: jsonb("payload")
|
|
25
|
+
.$type()
|
|
26
|
+
.notNull(),
|
|
27
|
+
actorId: text("actor_id"),
|
|
28
|
+
entityType: varchar("entity_type", { length: 191 }),
|
|
29
|
+
entityId: text("entity_id"),
|
|
30
|
+
idempotencyKey: varchar("idempotency_key", { length: 255 }),
|
|
31
|
+
scheduledFor: timestamp("scheduled_for", {
|
|
32
|
+
withTimezone: true,
|
|
33
|
+
mode: "date",
|
|
34
|
+
})
|
|
35
|
+
.notNull()
|
|
36
|
+
.defaultNow(),
|
|
37
|
+
readAt: timestamp("read_at", { withTimezone: true, mode: "date" }),
|
|
38
|
+
archivedAt: timestamp("archived_at", {
|
|
39
|
+
withTimezone: true,
|
|
40
|
+
mode: "date",
|
|
41
|
+
}),
|
|
42
|
+
canceledAt: timestamp("canceled_at", {
|
|
43
|
+
withTimezone: true,
|
|
44
|
+
mode: "date",
|
|
45
|
+
}),
|
|
46
|
+
createdAt: timestamp("created_at", {
|
|
47
|
+
withTimezone: true,
|
|
48
|
+
mode: "date",
|
|
49
|
+
})
|
|
50
|
+
.notNull()
|
|
51
|
+
.defaultNow(),
|
|
52
|
+
updatedAt: timestamp("updated_at", {
|
|
53
|
+
withTimezone: true,
|
|
54
|
+
mode: "date",
|
|
55
|
+
})
|
|
56
|
+
.notNull()
|
|
57
|
+
.defaultNow(),
|
|
58
|
+
}, (columns) => [
|
|
59
|
+
index(name("notifications_recipient_created_idx")).on(columns.recipientId, columns.createdAt.desc()),
|
|
60
|
+
index(name("notifications_recipient_unread_idx"))
|
|
61
|
+
.on(columns.recipientId, columns.createdAt.desc())
|
|
62
|
+
.where(sql `${columns.readAt} is null and ${columns.archivedAt} is null`),
|
|
63
|
+
uniqueIndex(name("notifications_recipient_idempotency_uidx"))
|
|
64
|
+
.on(columns.recipientId, columns.idempotencyKey)
|
|
65
|
+
.where(sql `${columns.idempotencyKey} is not null`),
|
|
66
|
+
]);
|
|
67
|
+
const deliveries = table("deliveries", {
|
|
68
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
69
|
+
notificationId: uuid("notification_id")
|
|
70
|
+
.notNull()
|
|
71
|
+
.references(() => notifications.id, { onDelete: "cascade" }),
|
|
72
|
+
channel: varchar("channel", { length: 64 }).notNull(),
|
|
73
|
+
status: varchar("status", { length: 20 })
|
|
74
|
+
.$type()
|
|
75
|
+
.notNull()
|
|
76
|
+
.default("pending"),
|
|
77
|
+
attempts: integer("attempts").notNull().default(0),
|
|
78
|
+
maxAttempts: integer("max_attempts").notNull().default(5),
|
|
79
|
+
nextAttemptAt: timestamp("next_attempt_at", {
|
|
80
|
+
withTimezone: true,
|
|
81
|
+
mode: "date",
|
|
82
|
+
}),
|
|
83
|
+
lastAttemptAt: timestamp("last_attempt_at", {
|
|
84
|
+
withTimezone: true,
|
|
85
|
+
mode: "date",
|
|
86
|
+
}),
|
|
87
|
+
lockedAt: timestamp("locked_at", {
|
|
88
|
+
withTimezone: true,
|
|
89
|
+
mode: "date",
|
|
90
|
+
}),
|
|
91
|
+
lockedBy: varchar("locked_by", { length: 191 }),
|
|
92
|
+
sentAt: timestamp("sent_at", { withTimezone: true, mode: "date" }),
|
|
93
|
+
providerMessageId: text("provider_message_id"),
|
|
94
|
+
errorCode: varchar("error_code", { length: 191 }),
|
|
95
|
+
errorMessage: text("error_message"),
|
|
96
|
+
metadata: jsonb("metadata").$type(),
|
|
97
|
+
createdAt: timestamp("created_at", {
|
|
98
|
+
withTimezone: true,
|
|
99
|
+
mode: "date",
|
|
100
|
+
})
|
|
101
|
+
.notNull()
|
|
102
|
+
.defaultNow(),
|
|
103
|
+
updatedAt: timestamp("updated_at", {
|
|
104
|
+
withTimezone: true,
|
|
105
|
+
mode: "date",
|
|
106
|
+
})
|
|
107
|
+
.notNull()
|
|
108
|
+
.defaultNow(),
|
|
109
|
+
}, (columns) => [
|
|
110
|
+
uniqueIndex(name("deliveries_notification_channel_uidx")).on(columns.notificationId, columns.channel),
|
|
111
|
+
index(name("deliveries_due_idx"))
|
|
112
|
+
.on(columns.nextAttemptAt, columns.createdAt)
|
|
113
|
+
.where(sql `${columns.status} in ('pending', 'retrying', 'processing')`),
|
|
114
|
+
check(name("deliveries_status_check"), sql `${columns.status} in ('pending', 'processing', 'retrying', 'sent', 'skipped', 'failed', 'canceled')`),
|
|
115
|
+
check(name("deliveries_attempts_check"), sql `${columns.attempts} >= 0 and ${columns.maxAttempts} > 0`),
|
|
116
|
+
]);
|
|
117
|
+
const preferences = table("preferences", {
|
|
118
|
+
recipientId: text("recipient_id").notNull(),
|
|
119
|
+
topic: varchar("topic", { length: 191 }).notNull(),
|
|
120
|
+
channel: varchar("channel", { length: 64 }).notNull(),
|
|
121
|
+
enabled: boolean("enabled").notNull(),
|
|
122
|
+
createdAt: timestamp("created_at", {
|
|
123
|
+
withTimezone: true,
|
|
124
|
+
mode: "date",
|
|
125
|
+
})
|
|
126
|
+
.notNull()
|
|
127
|
+
.defaultNow(),
|
|
128
|
+
updatedAt: timestamp("updated_at", {
|
|
129
|
+
withTimezone: true,
|
|
130
|
+
mode: "date",
|
|
131
|
+
})
|
|
132
|
+
.notNull()
|
|
133
|
+
.defaultNow(),
|
|
134
|
+
}, (columns) => [
|
|
135
|
+
primaryKey({
|
|
136
|
+
name: name("preferences_pk"),
|
|
137
|
+
columns: [columns.recipientId, columns.topic, columns.channel],
|
|
138
|
+
}),
|
|
139
|
+
index(name("preferences_recipient_idx")).on(columns.recipientId),
|
|
140
|
+
]);
|
|
141
|
+
return {
|
|
142
|
+
notifications,
|
|
143
|
+
notificationDeliveries: deliveries,
|
|
144
|
+
notificationPreferences: preferences,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export const notificationSchema = createNotificationSchema();
|
|
148
|
+
export const { notifications, notificationDeliveries, notificationPreferences, } = notificationSchema;
|
|
149
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export type JsonPrimitive = boolean | number | string | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export type JsonObject = {
|
|
6
|
+
[key: string]: JsonValue;
|
|
7
|
+
};
|
|
8
|
+
export declare const BUILT_IN_CHANNELS: {
|
|
9
|
+
readonly IN_APP: "in_app";
|
|
10
|
+
readonly EMAIL: "email";
|
|
11
|
+
readonly SMS: "sms";
|
|
12
|
+
readonly PUSH: "push";
|
|
13
|
+
};
|
|
14
|
+
export type BuiltInChannel = (typeof BUILT_IN_CHANNELS)[keyof typeof BUILT_IN_CHANNELS];
|
|
15
|
+
export declare const PREFERENCE_WILDCARD: "*";
|
|
16
|
+
export type DeliveryStatus = "pending" | "processing" | "retrying" | "sent" | "skipped" | "failed" | "canceled";
|
|
17
|
+
export type NotificationPayload<TChannel extends string = string, TData extends JsonObject = JsonObject> = {
|
|
18
|
+
title: string;
|
|
19
|
+
body: string;
|
|
20
|
+
actionUrl?: string;
|
|
21
|
+
data?: TData;
|
|
22
|
+
channelData?: Partial<Record<TChannel, JsonValue>>;
|
|
23
|
+
};
|
|
24
|
+
export type DeliveryResult = {
|
|
25
|
+
providerMessageId?: string;
|
|
26
|
+
metadata?: JsonObject;
|
|
27
|
+
};
|
|
28
|
+
export type DeliveryContext<TChannel extends string = string, TData extends JsonObject = JsonObject> = {
|
|
29
|
+
notificationId: string;
|
|
30
|
+
deliveryId: string;
|
|
31
|
+
recipientId: string;
|
|
32
|
+
topic: string;
|
|
33
|
+
channel: TChannel;
|
|
34
|
+
payload: NotificationPayload<TChannel, TData>;
|
|
35
|
+
channelData: JsonValue | undefined;
|
|
36
|
+
attempt: number;
|
|
37
|
+
maxAttempts: number;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
scheduledFor: Date;
|
|
40
|
+
actorId: string | null;
|
|
41
|
+
entityType: string | null;
|
|
42
|
+
entityId: string | null;
|
|
43
|
+
};
|
|
44
|
+
export type DeliveryAdapter<TChannel extends string = string, TData extends JsonObject = JsonObject> = (context: DeliveryContext<TChannel, TData>) => Promise<DeliveryResult | void>;
|
|
45
|
+
export type DeliveryAdapters<TChannel extends string, TData extends JsonObject = JsonObject> = Partial<{
|
|
46
|
+
[TKey in TChannel]: DeliveryAdapter<TKey, TData>;
|
|
47
|
+
}>;
|
|
48
|
+
export type PreferenceDefaults<TChannel extends string> = Record<string, Partial<Record<TChannel, boolean>>>;
|
|
49
|
+
export type PreferenceOverride<TChannel extends string = string> = {
|
|
50
|
+
recipientId: string;
|
|
51
|
+
topic: string | typeof PREFERENCE_WILDCARD;
|
|
52
|
+
channel: TChannel | typeof PREFERENCE_WILDCARD;
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
};
|
|
55
|
+
export type ResolvedPreference<TChannel extends string = string> = {
|
|
56
|
+
topic: string;
|
|
57
|
+
channel: TChannel;
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
source: "exact" | "topic" | "channel" | "recipient" | "topic_default" | "global_default";
|
|
60
|
+
};
|
|
61
|
+
export type PublishInput<TChannel extends string, TData extends JsonObject = JsonObject> = {
|
|
62
|
+
recipientIds: readonly string[];
|
|
63
|
+
topic: string;
|
|
64
|
+
payload: NotificationPayload<TChannel, TData>;
|
|
65
|
+
channels?: readonly TChannel[];
|
|
66
|
+
actorId?: string | null;
|
|
67
|
+
entityType?: string | null;
|
|
68
|
+
entityId?: string | null;
|
|
69
|
+
idempotencyKey?: string;
|
|
70
|
+
scheduledFor?: Date;
|
|
71
|
+
preferencePolicy?: "respect" | "ignore";
|
|
72
|
+
maxAttempts?: number | Partial<Record<TChannel, number>>;
|
|
73
|
+
};
|
|
74
|
+
export type PublishedNotification<TChannel extends string = string> = {
|
|
75
|
+
notificationId: string;
|
|
76
|
+
recipientId: string;
|
|
77
|
+
created: boolean;
|
|
78
|
+
deliveries: Array<{
|
|
79
|
+
deliveryId: string;
|
|
80
|
+
channel: TChannel;
|
|
81
|
+
status: DeliveryStatus;
|
|
82
|
+
}>;
|
|
83
|
+
};
|
|
84
|
+
export type DispatchResult<TChannel extends string = string> = {
|
|
85
|
+
deliveryId: string;
|
|
86
|
+
notificationId: string;
|
|
87
|
+
recipientId: string;
|
|
88
|
+
channel: TChannel;
|
|
89
|
+
status: "sent" | "retrying" | "failed";
|
|
90
|
+
attempt: number;
|
|
91
|
+
errorCode?: string;
|
|
92
|
+
errorMessage?: string;
|
|
93
|
+
};
|
|
94
|
+
export type InboxItem<TData extends JsonObject = JsonObject> = {
|
|
95
|
+
id: string;
|
|
96
|
+
recipientId: string;
|
|
97
|
+
topic: string;
|
|
98
|
+
title: string;
|
|
99
|
+
body: string;
|
|
100
|
+
actionUrl: string | null;
|
|
101
|
+
data: TData;
|
|
102
|
+
actorId: string | null;
|
|
103
|
+
entityType: string | null;
|
|
104
|
+
entityId: string | null;
|
|
105
|
+
readAt: Date | null;
|
|
106
|
+
archivedAt: Date | null;
|
|
107
|
+
createdAt: Date;
|
|
108
|
+
};
|
|
109
|
+
export type InboxCursor = {
|
|
110
|
+
createdAt: Date;
|
|
111
|
+
id: string;
|
|
112
|
+
};
|
|
113
|
+
export type InboxPage<TData extends JsonObject = JsonObject> = {
|
|
114
|
+
items: Array<InboxItem<TData>>;
|
|
115
|
+
nextCursor: InboxCursor | null;
|
|
116
|
+
};
|
|
117
|
+
export type NotificationLogger = {
|
|
118
|
+
debug?: (message: string, context?: JsonObject) => void;
|
|
119
|
+
error?: (message: string, context?: JsonObject) => void;
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;CACJ,CAAC;AAKX,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAY,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Adoption notes
|
|
2
|
+
|
|
3
|
+
The package was shaped against two existing PostgreSQL/Drizzle applications
|
|
4
|
+
with complementary notification capabilities.
|
|
5
|
+
|
|
6
|
+
## Application with Web Push
|
|
7
|
+
|
|
8
|
+
Keep Push subscription persistence and VAPID delivery in the application.
|
|
9
|
+
Register its existing `sendPushToUsers()` function as the `push` adapter.
|
|
10
|
+
Incoming-call and online-presence features publish topics through UNC,
|
|
11
|
+
which adds preference checks, durable delivery status, idempotency, and an
|
|
12
|
+
optional in-app companion.
|
|
13
|
+
|
|
14
|
+
UNC does not replace Web Push endpoint cleanup. A 404/410 response is
|
|
15
|
+
provider-specific subscription lifecycle behavior and remains in the adapter.
|
|
16
|
+
|
|
17
|
+
## CRM with email and SMS infrastructure
|
|
18
|
+
|
|
19
|
+
Keep mailbox/domain verification, suppressions, SMTP retry rows, SMS carriers,
|
|
20
|
+
virtual SIM selection, and device OTP handling in the CRM. Register:
|
|
21
|
+
|
|
22
|
+
- `enqueueEmail()` or `sendEmail()` as the `email` adapter;
|
|
23
|
+
- the selected carrier/queue function as the `sms` adapter.
|
|
24
|
+
|
|
25
|
+
If the existing mail function enqueues durably, the UNC delivery becomes
|
|
26
|
+
`sent` when enqueue succeeds. SMTP attempts and bounce events remain in the
|
|
27
|
+
mail subsystem. The returned mail row id can be stored as
|
|
28
|
+
`providerMessageId`.
|
|
29
|
+
|
|
30
|
+
The CRM's existing reminder rows are a domain scheduling feature (entity/date/
|
|
31
|
+
remind-before semantics). They should publish a UNC notification when due, not
|
|
32
|
+
be silently reinterpreted as UNC delivery rows.
|
|
33
|
+
|
|
34
|
+
## Deliberate non-goals
|
|
35
|
+
|
|
36
|
+
- No provider account/domain management.
|
|
37
|
+
- No contact database.
|
|
38
|
+
- No template editor or renderer.
|
|
39
|
+
- No audience/campaign builder.
|
|
40
|
+
- No HTTP routes or UI.
|
|
41
|
+
- No automatic migration of existing application tables.
|
|
42
|
+
- No in-process singleton queue.
|
|
43
|
+
|
|
44
|
+
Those capabilities belong to consuming applications or focused provider
|
|
45
|
+
packages.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Data ownership
|
|
4
|
+
|
|
5
|
+
UNC owns three tables:
|
|
6
|
+
|
|
7
|
+
- `unc_notifications`: one logical notification per recipient;
|
|
8
|
+
- `unc_deliveries`: one delivery per notification and selected channel;
|
|
9
|
+
- `unc_preferences`: sparse recipient overrides by topic and channel.
|
|
10
|
+
|
|
11
|
+
One notification per recipient is intentional. Read/archive state and
|
|
12
|
+
idempotency are recipient-specific, and it avoids arrays of recipient ids that
|
|
13
|
+
are difficult to authorize or index.
|
|
14
|
+
|
|
15
|
+
## Delivery lifecycle
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
pending -> processing -> sent
|
|
19
|
+
\-> retrying -> processing
|
|
20
|
+
\-> failed
|
|
21
|
+
pending/retrying/processing -> canceled
|
|
22
|
+
selected but opted out -> skipped
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Publishing and creating delivery rows occurs in one database transaction.
|
|
26
|
+
Provider callbacks never run inside that transaction.
|
|
27
|
+
|
|
28
|
+
`dispatchDue()` claims a bounded set of due rows in a transaction using
|
|
29
|
+
`FOR UPDATE SKIP LOCKED`, changes them to `processing`, commits, and only then
|
|
30
|
+
invokes adapters. The `lockedBy` token prevents a late worker from overwriting a
|
|
31
|
+
row reclaimed after its lock expired.
|
|
32
|
+
|
|
33
|
+
## In-app as a channel
|
|
34
|
+
|
|
35
|
+
`in_app` uses the same preference and scheduling path as every external
|
|
36
|
+
channel. Its delivery needs no callback: successfully processing it makes the
|
|
37
|
+
notification visible to inbox queries.
|
|
38
|
+
|
|
39
|
+
This avoids the common bug where creating an email-only notification
|
|
40
|
+
accidentally inserts an in-app alert.
|
|
41
|
+
|
|
42
|
+
## Preference model
|
|
43
|
+
|
|
44
|
+
Only explicit recipient overrides are stored. Application defaults live in
|
|
45
|
+
code, versioned with application behavior. Wildcards make broad choices easy
|
|
46
|
+
without copying every topic/channel combination into the database.
|
|
47
|
+
|
|
48
|
+
Preference checks occur at publication time. The resolved choice is captured in
|
|
49
|
+
the delivery row as `pending` or `skipped`, so changing a preference later does
|
|
50
|
+
not rewrite historical notifications.
|
|
51
|
+
|
|
52
|
+
## Provider boundary
|
|
53
|
+
|
|
54
|
+
Adapters receive:
|
|
55
|
+
|
|
56
|
+
- UNC notification and delivery ids;
|
|
57
|
+
- recipient id;
|
|
58
|
+
- topic and canonical payload;
|
|
59
|
+
- channel-specific JSON;
|
|
60
|
+
- attempt/max-attempt counts;
|
|
61
|
+
- actor/entity context.
|
|
62
|
+
|
|
63
|
+
The application resolves current contact information and credentials. That
|
|
64
|
+
keeps personally identifiable addresses out of UNC tables and prevents
|
|
65
|
+
stale addresses from being baked into scheduled notifications.
|
|
66
|
+
|
|
67
|
+
## Delivery semantics
|
|
68
|
+
|
|
69
|
+
UNC provides at-least-once callback invocation. A process can exit
|
|
70
|
+
after a provider accepts a request but before the delivery row is marked sent.
|
|
71
|
+
Applications should pass `deliveryId` as a provider idempotency key whenever the
|
|
72
|
+
provider supports it.
|
|
73
|
+
|
|
74
|
+
Exactly-once delivery cannot be guaranteed across a database and an external
|
|
75
|
+
provider without provider-side idempotency or a distributed transaction.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILT_IN_CHANNELS,
|
|
3
|
+
createNotificationCore,
|
|
4
|
+
type NotificationDeliveryError,
|
|
5
|
+
} from "unified-notification-core";
|
|
6
|
+
|
|
7
|
+
import { db } from "./db.js";
|
|
8
|
+
import { notificationSchema } from "./notification-schema.js";
|
|
9
|
+
|
|
10
|
+
type AppChannel =
|
|
11
|
+
| typeof BUILT_IN_CHANNELS.IN_APP
|
|
12
|
+
| typeof BUILT_IN_CHANNELS.EMAIL
|
|
13
|
+
| typeof BUILT_IN_CHANNELS.SMS
|
|
14
|
+
| typeof BUILT_IN_CHANNELS.PUSH;
|
|
15
|
+
|
|
16
|
+
declare function sendMail(input: {
|
|
17
|
+
userId: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}): Promise<{ id: string }>;
|
|
21
|
+
|
|
22
|
+
declare function sendSMS(input: {
|
|
23
|
+
userId: string;
|
|
24
|
+
text: string;
|
|
25
|
+
}): Promise<{ id: string }>;
|
|
26
|
+
|
|
27
|
+
declare function sendPush(
|
|
28
|
+
userId: string,
|
|
29
|
+
payload: { title: string; body: string; actionUrl?: string },
|
|
30
|
+
): Promise<{ sent: number }>;
|
|
31
|
+
|
|
32
|
+
const core = createNotificationCore<AppChannel>({
|
|
33
|
+
db,
|
|
34
|
+
schema: notificationSchema,
|
|
35
|
+
adapters: {
|
|
36
|
+
email: async ({ recipientId, payload }) => {
|
|
37
|
+
const result = await sendMail({
|
|
38
|
+
userId: recipientId,
|
|
39
|
+
subject: payload.title,
|
|
40
|
+
text: payload.body,
|
|
41
|
+
});
|
|
42
|
+
return { providerMessageId: result.id };
|
|
43
|
+
},
|
|
44
|
+
sms: async ({ recipientId, payload }) => {
|
|
45
|
+
const result = await sendSMS({
|
|
46
|
+
userId: recipientId,
|
|
47
|
+
text: payload.body,
|
|
48
|
+
});
|
|
49
|
+
return { providerMessageId: result.id };
|
|
50
|
+
},
|
|
51
|
+
push: async ({ recipientId, payload }) => {
|
|
52
|
+
const result = await sendPush(recipientId, payload);
|
|
53
|
+
return { metadata: { sentDevices: result.sent } };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultChannels: ["in_app", "push"],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await core.publishAndDispatch({
|
|
60
|
+
recipientIds: ["user-123"],
|
|
61
|
+
topic: "appointment.reminder",
|
|
62
|
+
idempotencyKey: "appointment-456:reminder-24h",
|
|
63
|
+
payload: {
|
|
64
|
+
title: "Appointment tomorrow",
|
|
65
|
+
body: "Your appointment starts at 10:00.",
|
|
66
|
+
actionUrl: "/appointments/456",
|
|
67
|
+
data: { appointmentId: "456" },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Importing the error as a type above is only to show that adapters may use
|
|
72
|
+
// NotificationDeliveryError for permanent failures.
|
|
73
|
+
type PermanentFailure = NotificationDeliveryError;
|
|
74
|
+
void (null as PermanentFailure | null);
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "unified-notification-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UNC is a Drizzle-native multichannel notification core with durable delivery, retries, in-app inboxes, and recipient preferences.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./schema": {
|
|
14
|
+
"types": "./dist/schema.d.ts",
|
|
15
|
+
"import": "./dist/schema.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"docs",
|
|
21
|
+
"examples",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"check": "npm run typecheck && npm run test && npm run build && npm pack --dry-run",
|
|
31
|
+
"prepare": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"unc",
|
|
35
|
+
"notifications",
|
|
36
|
+
"drizzle",
|
|
37
|
+
"postgresql",
|
|
38
|
+
"email",
|
|
39
|
+
"sms",
|
|
40
|
+
"push",
|
|
41
|
+
"in-app",
|
|
42
|
+
"outbox"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"drizzle-orm": ">=0.45.0 <1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@electric-sql/pglite": "^0.5.4",
|
|
53
|
+
"@types/node": "^26.1.2",
|
|
54
|
+
"drizzle-orm": "^0.45.2",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"vitest": "^4.1.10"
|
|
57
|
+
}
|
|
58
|
+
}
|