easy-ping 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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/dist/adapter-f0OR2DhY.d.cts +200 -0
  4. package/dist/adapter-f0OR2DhY.d.ts +200 -0
  5. package/dist/adapters/drizzle.cjs +440 -0
  6. package/dist/adapters/drizzle.cjs.map +1 -0
  7. package/dist/adapters/drizzle.d.cts +991 -0
  8. package/dist/adapters/drizzle.d.ts +991 -0
  9. package/dist/adapters/drizzle.js +409 -0
  10. package/dist/adapters/drizzle.js.map +1 -0
  11. package/dist/adapters/mongodb.cjs +458 -0
  12. package/dist/adapters/mongodb.cjs.map +1 -0
  13. package/dist/adapters/mongodb.d.cts +32 -0
  14. package/dist/adapters/mongodb.d.ts +32 -0
  15. package/dist/adapters/mongodb.js +376 -0
  16. package/dist/adapters/mongodb.js.map +1 -0
  17. package/dist/browser.cjs +96 -0
  18. package/dist/browser.cjs.map +1 -0
  19. package/dist/browser.d.cts +45 -0
  20. package/dist/browser.d.ts +45 -0
  21. package/dist/browser.js +89 -0
  22. package/dist/browser.js.map +1 -0
  23. package/dist/chunk-BV3U542P.js +11 -0
  24. package/dist/chunk-BV3U542P.js.map +1 -0
  25. package/dist/chunk-NYDVNMKG.js +62 -0
  26. package/dist/chunk-NYDVNMKG.js.map +1 -0
  27. package/dist/chunk-OZSOIX23.js +120 -0
  28. package/dist/chunk-OZSOIX23.js.map +1 -0
  29. package/dist/chunk-PTGHMCCG.js +23 -0
  30. package/dist/chunk-PTGHMCCG.js.map +1 -0
  31. package/dist/chunk-W3BMEPPQ.js +50 -0
  32. package/dist/chunk-W3BMEPPQ.js.map +1 -0
  33. package/dist/client.cjs +160 -0
  34. package/dist/client.cjs.map +1 -0
  35. package/dist/client.d.cts +42 -0
  36. package/dist/client.d.ts +42 -0
  37. package/dist/client.js +158 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/index.cjs +920 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.cts +203 -0
  42. package/dist/index.d.ts +203 -0
  43. package/dist/index.js +723 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/node.cjs +49 -0
  46. package/dist/node.cjs.map +1 -0
  47. package/dist/node.d.cts +8 -0
  48. package/dist/node.d.ts +8 -0
  49. package/dist/node.js +46 -0
  50. package/dist/node.js.map +1 -0
  51. package/dist/plugin-Bqj9xAOS.d.cts +217 -0
  52. package/dist/plugin-bTJKmnlq.d.ts +217 -0
  53. package/dist/plugins/digests.cjs +206 -0
  54. package/dist/plugins/digests.cjs.map +1 -0
  55. package/dist/plugins/digests.d.cts +68 -0
  56. package/dist/plugins/digests.d.ts +68 -0
  57. package/dist/plugins/digests.js +201 -0
  58. package/dist/plugins/digests.js.map +1 -0
  59. package/dist/plugins/preferences-client.cjs +23 -0
  60. package/dist/plugins/preferences-client.cjs.map +1 -0
  61. package/dist/plugins/preferences-client.d.cts +29 -0
  62. package/dist/plugins/preferences-client.d.ts +29 -0
  63. package/dist/plugins/preferences-client.js +18 -0
  64. package/dist/plugins/preferences-client.js.map +1 -0
  65. package/dist/plugins/preferences-react.cjs +125 -0
  66. package/dist/plugins/preferences-react.cjs.map +1 -0
  67. package/dist/plugins/preferences-react.d.cts +40 -0
  68. package/dist/plugins/preferences-react.d.ts +40 -0
  69. package/dist/plugins/preferences-react.js +101 -0
  70. package/dist/plugins/preferences-react.js.map +1 -0
  71. package/dist/plugins/preferences.cjs +147 -0
  72. package/dist/plugins/preferences.cjs.map +1 -0
  73. package/dist/plugins/preferences.d.cts +22 -0
  74. package/dist/plugins/preferences.d.ts +22 -0
  75. package/dist/plugins/preferences.js +119 -0
  76. package/dist/plugins/preferences.js.map +1 -0
  77. package/dist/plugins/push.cjs +146 -0
  78. package/dist/plugins/push.cjs.map +1 -0
  79. package/dist/plugins/push.d.cts +60 -0
  80. package/dist/plugins/push.d.ts +60 -0
  81. package/dist/plugins/push.js +144 -0
  82. package/dist/plugins/push.js.map +1 -0
  83. package/dist/provider-CA45WVqt.d.cts +23 -0
  84. package/dist/provider-CA45WVqt.d.ts +23 -0
  85. package/dist/providers/resend.cjs +69 -0
  86. package/dist/providers/resend.cjs.map +1 -0
  87. package/dist/providers/resend.d.cts +20 -0
  88. package/dist/providers/resend.d.ts +20 -0
  89. package/dist/providers/resend.js +66 -0
  90. package/dist/providers/resend.js.map +1 -0
  91. package/dist/providers/web-push.cjs +211 -0
  92. package/dist/providers/web-push.cjs.map +1 -0
  93. package/dist/providers/web-push.d.cts +33 -0
  94. package/dist/providers/web-push.d.ts +33 -0
  95. package/dist/providers/web-push.js +193 -0
  96. package/dist/providers/web-push.js.map +1 -0
  97. package/dist/react.cjs +218 -0
  98. package/dist/react.cjs.map +1 -0
  99. package/dist/react.d.cts +57 -0
  100. package/dist/react.d.ts +57 -0
  101. package/dist/react.js +193 -0
  102. package/dist/react.js.map +1 -0
  103. package/dist/schema.cjs +263 -0
  104. package/dist/schema.cjs.map +1 -0
  105. package/dist/schema.d.cts +180 -0
  106. package/dist/schema.d.ts +180 -0
  107. package/dist/schema.js +200 -0
  108. package/dist/schema.js.map +1 -0
  109. package/dist/testing.cjs +342 -0
  110. package/dist/testing.cjs.map +1 -0
  111. package/dist/testing.d.cts +38 -0
  112. package/dist/testing.d.ts +38 -0
  113. package/dist/testing.js +340 -0
  114. package/dist/testing.js.map +1 -0
  115. package/package.json +168 -0
@@ -0,0 +1,200 @@
1
+ type Channel = "inApp" | "email" | "push" | "sms" | "slack";
2
+ type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
3
+ type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
4
+ type Frequency = "instant" | "daily" | "weekly" | "off";
5
+ type SkipReason = "deduped" | "no-channels" | "no-recipient"
6
+ /** Every declared channel lacks a provider — a misconfiguration, not a preference. */
7
+ | "channel-unavailable";
8
+ type Recipient = {
9
+ userId: string;
10
+ email?: string | undefined;
11
+ phone?: string | undefined;
12
+ pushTokens?: readonly string[] | undefined;
13
+ timezone: string;
14
+ locale: string;
15
+ };
16
+ type NotificationRecord = {
17
+ id: string;
18
+ userId: string;
19
+ type: string;
20
+ payload: unknown;
21
+ actorId: string | null;
22
+ groupKey: string | null;
23
+ dedupeKey: string | null;
24
+ seenAt: Date | null;
25
+ readAt: Date | null;
26
+ archivedAt: Date | null;
27
+ createdAt: Date;
28
+ };
29
+ type DeliveryRecord = {
30
+ id: string;
31
+ notificationId: string;
32
+ channel: Channel;
33
+ status: DeliveryStatus;
34
+ attempts: number;
35
+ maxAttempts: number;
36
+ notBefore: Date;
37
+ claimedAt: Date | null;
38
+ claimedBy: string | null;
39
+ lastError: string | null;
40
+ updatedAt: Date;
41
+ };
42
+
43
+ type Scalar = string | number | boolean | Date | null;
44
+ /**
45
+ * One operator per condition. Operators are tagged objects so a bare value is
46
+ * always an equality test; json columns therefore cannot be filtered, since an
47
+ * object value would be ambiguous.
48
+ */
49
+ type WhereCondition = Scalar | {
50
+ in: readonly (string | number)[];
51
+ } | {
52
+ lt: Date | number;
53
+ } | {
54
+ lte: Date | number;
55
+ } | {
56
+ gt: Date | number;
57
+ } | {
58
+ gte: Date | number;
59
+ } | {
60
+ not: Scalar;
61
+ };
62
+ type WhereClause = Record<string, WhereCondition>;
63
+ type QueryOptions = {
64
+ limit?: number;
65
+ orderBy?: {
66
+ field: string;
67
+ direction: "asc" | "desc";
68
+ };
69
+ };
70
+ type UpsertOptions = {
71
+ onConflict: readonly string[];
72
+ };
73
+ /**
74
+ * Table access for plugins, scoped to the tables that plugin declared.
75
+ *
76
+ * Plugins could always declare tables via schema() but had no way to read
77
+ * them, so preferences had to bolt its queries onto the core adapter. That
78
+ * does not generalise to digests or push.
79
+ */
80
+ type PluginStore = {
81
+ find<T = Record<string, unknown>>(table: string, where?: WhereClause, options?: QueryOptions): Promise<T[]>;
82
+ insert(table: string, rows: readonly Record<string, unknown>[]): Promise<number>;
83
+ upsert(table: string, rows: readonly Record<string, unknown>[], options: UpsertOptions): Promise<number>;
84
+ update(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;
85
+ remove(table: string, where: WhereClause): Promise<number>;
86
+ };
87
+
88
+ type PreferenceRecord = {
89
+ userId: string;
90
+ type: string;
91
+ channel: Channel;
92
+ enabled: boolean;
93
+ frequency: Frequency;
94
+ };
95
+ type InsertNotification = {
96
+ id: string;
97
+ userId: string;
98
+ type: string;
99
+ payload: unknown;
100
+ actorId?: string | undefined;
101
+ groupKey?: string | undefined;
102
+ dedupeKey?: string | undefined;
103
+ deliveries: readonly InsertDelivery[];
104
+ };
105
+ type InsertDelivery = {
106
+ id: string;
107
+ channel: Channel;
108
+ maxAttempts: number;
109
+ notBefore: Date;
110
+ };
111
+ type ClaimArgs = {
112
+ limit: number;
113
+ leaseMs: number;
114
+ /** Unique per call, not per worker. See RFC 0003 §4. */
115
+ claimToken: string;
116
+ channels?: readonly Channel[] | undefined;
117
+ /** Scopes the claim to one send; without it inline/deferred flush the whole table. */
118
+ ids?: readonly string[] | undefined;
119
+ now?: Date | undefined;
120
+ };
121
+ type ClaimedDelivery = {
122
+ id: string;
123
+ notificationId: string;
124
+ channel: Channel;
125
+ attempts: number;
126
+ maxAttempts: number;
127
+ notification: {
128
+ userId: string;
129
+ type: string;
130
+ payload: unknown;
131
+ actorId: string | null;
132
+ };
133
+ };
134
+ type DeliveryOutcome = {
135
+ result: "sent";
136
+ } | {
137
+ result: "failed";
138
+ error: string;
139
+ retryable: boolean;
140
+ };
141
+ type DeliveryRelease = {
142
+ id: string;
143
+ claimToken: string;
144
+ outcome: DeliveryOutcome;
145
+ /** Applied when a retryable failure re-arms the row. */
146
+ nextAttemptAt?: Date | undefined;
147
+ };
148
+ type FeedQuery = {
149
+ userId: string;
150
+ limit: number;
151
+ cursor?: string | undefined;
152
+ unreadOnly?: boolean | undefined;
153
+ };
154
+ type FeedPage = {
155
+ notifications: readonly NotificationRecord[];
156
+ nextCursor: string | null;
157
+ };
158
+ type DatabaseAdapter = {
159
+ readonly name: string;
160
+ /**
161
+ * How the adapter names stored fields. SQL adapters use snake_case columns;
162
+ * document stores keep the declared camelCase. The plugin store translates
163
+ * accordingly rather than assuming one of them.
164
+ */
165
+ readonly naming?: "snake_case" | "preserve";
166
+ /**
167
+ * True when json values must reach the driver as strings. postgres-js cannot
168
+ * bind a plain object; Mongo stores one natively.
169
+ */
170
+ readonly serializesJson?: boolean;
171
+ /** One transaction. A duplicate (userId, dedupeKey) is reported, not thrown. */
172
+ createNotifications(rows: readonly InsertNotification[]): Promise<{
173
+ created: readonly string[];
174
+ deduped: readonly string[];
175
+ }>;
176
+ /** Atomic lease: a returned row stays invisible to other callers until it expires. */
177
+ claimPendingDeliveries(args: ClaimArgs): Promise<readonly ClaimedDelivery[]>;
178
+ /** Writes terminal state even when claimToken is stale. See RFC 0003 §6. */
179
+ releaseDeliveries(releases: readonly DeliveryRelease[]): Promise<void>;
180
+ listNotifications(query: FeedQuery): Promise<FeedPage>;
181
+ countUnseen(userId: string): Promise<number>;
182
+ markSeen(userId: string, before: Date): Promise<void>;
183
+ markRead(userId: string, notificationIds: readonly string[]): Promise<number>;
184
+ markAllRead(userId: string): Promise<number>;
185
+ getFailedDeliveries(args: {
186
+ since: Date;
187
+ limit: number;
188
+ }): Promise<readonly DeliveryRecord[]>;
189
+ /**
190
+ * Generic table access for plugin-declared tables. Reached only through a
191
+ * PluginStore, which validates the table and every column against the
192
+ * plugin's own schema() before anything gets here.
193
+ */
194
+ queryTable(table: string, where: WhereClause, options: QueryOptions): Promise<Record<string, unknown>[]>;
195
+ insertRows(table: string, rows: readonly Record<string, unknown>[], onConflict?: readonly string[]): Promise<number>;
196
+ updateRows(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;
197
+ deleteRows(table: string, where: WhereClause): Promise<number>;
198
+ };
199
+
200
+ export type { Channel as C, DatabaseAdapter as D, Frequency as F, InsertDelivery as I, NotificationRecord as N, PreferenceRecord as P, Recipient as R, SkipReason as S, DeliveryStatus as a, DeliveryMode as b, DeliveryRecord as c, ClaimArgs as d, ClaimedDelivery as e, DeliveryOutcome as f, DeliveryRelease as g, FeedPage as h, FeedQuery as i, InsertNotification as j, PluginStore as k };
@@ -0,0 +1,440 @@
1
+ 'use strict';
2
+
3
+ var drizzleOrm = require('drizzle-orm');
4
+ var pgCore = require('drizzle-orm/pg-core');
5
+
6
+ // src/adapters/drizzle/adapter.ts
7
+
8
+ // src/core/base64url.ts
9
+ function encodeBase64Url(input) {
10
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
11
+ let binary = "";
12
+ for (const byte of bytes) binary += String.fromCharCode(byte);
13
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
+ }
15
+ function decodeBase64UrlBytes(value) {
16
+ try {
17
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
18
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function decodeBase64Url(value) {
24
+ const bytes = decodeBase64UrlBytes(value);
25
+ return bytes ? new TextDecoder().decode(bytes) : null;
26
+ }
27
+
28
+ // src/schema/declaration.ts
29
+ var toSnakeCase = (value) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
30
+
31
+ // src/core/store.ts
32
+ var OPERATORS = ["in", "lt", "lte", "gt", "gte", "not"];
33
+ var isOperator = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && OPERATORS.some((op) => op in value);
34
+
35
+ // src/adapters/drizzle/adapter.ts
36
+ function toRows(result) {
37
+ if (Array.isArray(result)) return result;
38
+ if (result && typeof result === "object" && "rows" in result) {
39
+ const { rows } = result;
40
+ if (Array.isArray(rows)) return rows;
41
+ }
42
+ return [];
43
+ }
44
+ var str = (value) => String(value);
45
+ var nullableStr = (value) => value == null ? null : String(value);
46
+ var num = (value) => Number(value);
47
+ var date = (value) => value instanceof Date ? value : new Date(String(value));
48
+ var nullableDate = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
49
+ var ts = (value) => value.toISOString();
50
+ var encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
51
+ function decodeCursor(cursor) {
52
+ const decoded = decodeBase64Url(cursor);
53
+ if (!decoded) return null;
54
+ const [iso, id] = decoded.split("|");
55
+ if (!iso || !id) return null;
56
+ const createdAt = new Date(iso);
57
+ return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
58
+ }
59
+ function toNotification(row) {
60
+ return {
61
+ id: str(row.id),
62
+ userId: str(row.user_id),
63
+ type: str(row.type),
64
+ payload: row.payload,
65
+ actorId: nullableStr(row.actor_id),
66
+ groupKey: nullableStr(row.group_key),
67
+ dedupeKey: nullableStr(row.dedupe_key),
68
+ seenAt: nullableDate(row.seen_at),
69
+ readAt: nullableDate(row.read_at),
70
+ archivedAt: nullableDate(row.archived_at),
71
+ createdAt: date(row.created_at)
72
+ };
73
+ }
74
+ function toDelivery(row) {
75
+ return {
76
+ id: str(row.id),
77
+ notificationId: str(row.notification_id),
78
+ channel: str(row.channel),
79
+ status: str(row.status),
80
+ attempts: num(row.attempts),
81
+ maxAttempts: num(row.max_attempts),
82
+ notBefore: date(row.not_before),
83
+ claimedAt: nullableDate(row.claimed_at),
84
+ claimedBy: nullableStr(row.claimed_by),
85
+ lastError: nullableStr(row.last_error),
86
+ updatedAt: date(row.updated_at)
87
+ };
88
+ }
89
+ var bind = (value) => value instanceof Date ? value.toISOString() : value;
90
+ function whereSql(where) {
91
+ const entries = Object.entries(where);
92
+ if (entries.length === 0) return drizzleOrm.sql``;
93
+ const conditions = entries.map(([field, condition]) => {
94
+ const column = drizzleOrm.sql.identifier(toSnakeCase(field));
95
+ if (condition === null) return drizzleOrm.sql`${column} IS NULL`;
96
+ if (!isOperator(condition)) return drizzleOrm.sql`${column} = ${bind(condition)}`;
97
+ const operator = condition;
98
+ if ("in" in operator) {
99
+ const list = operator.in;
100
+ if (list.length === 0) return drizzleOrm.sql`FALSE`;
101
+ return drizzleOrm.sql`${column} IN (${drizzleOrm.sql.join(
102
+ list.map((item) => drizzleOrm.sql`${item}`),
103
+ drizzleOrm.sql`, `
104
+ )})`;
105
+ }
106
+ if ("lt" in operator) return drizzleOrm.sql`${column} < ${bind(operator.lt)}`;
107
+ if ("lte" in operator) return drizzleOrm.sql`${column} <= ${bind(operator.lte)}`;
108
+ if ("gt" in operator) return drizzleOrm.sql`${column} > ${bind(operator.gt)}`;
109
+ if ("gte" in operator) return drizzleOrm.sql`${column} >= ${bind(operator.gte)}`;
110
+ if ("not" in operator) {
111
+ return operator.not === null ? drizzleOrm.sql`${column} IS NOT NULL` : drizzleOrm.sql`${column} IS DISTINCT FROM ${bind(operator.not)}`;
112
+ }
113
+ return drizzleOrm.sql`TRUE`;
114
+ });
115
+ return drizzleOrm.sql` WHERE ${drizzleOrm.sql.join(conditions, drizzleOrm.sql` AND `)}`;
116
+ }
117
+ function drizzleAdapter(client, options = {}) {
118
+ const db = client;
119
+ const prefix = options.prefix ?? "";
120
+ const NOTIFICATION = drizzleOrm.sql.identifier(`${prefix}notification`);
121
+ const DELIVERY = drizzleOrm.sql.identifier(`${prefix}notification_delivery`);
122
+ const run = async (query, on = db) => toRows(await on.execute(query));
123
+ return {
124
+ name: "drizzle-pg",
125
+ naming: "snake_case",
126
+ serializesJson: true,
127
+ async createNotifications(input) {
128
+ if (input.length === 0) return { created: [], deduped: [] };
129
+ const now = /* @__PURE__ */ new Date();
130
+ return db.transaction(async (tx) => {
131
+ const values = input.map(
132
+ (row) => drizzleOrm.sql`(
133
+ ${row.id}::text,
134
+ ${row.userId}::text,
135
+ ${row.type}::text,
136
+ ${JSON.stringify(row.payload ?? null)}::jsonb,
137
+ ${row.actorId ?? null}::text,
138
+ ${row.groupKey ?? null}::text,
139
+ ${row.dedupeKey ?? null}::text,
140
+ ${ts(now)}::timestamptz
141
+ )`
142
+ );
143
+ const inserted = await run(
144
+ drizzleOrm.sql`
145
+ INSERT INTO ${NOTIFICATION} (id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at)
146
+ VALUES ${drizzleOrm.sql.join(values, drizzleOrm.sql`, `)}
147
+ ON CONFLICT (user_id, dedupe_key) DO NOTHING
148
+ RETURNING id
149
+ `,
150
+ tx
151
+ );
152
+ const created = new Set(inserted.map((row) => str(row.id)));
153
+ const deliveries = input.filter((row) => created.has(row.id)).flatMap(
154
+ (row) => row.deliveries.map(
155
+ (delivery) => drizzleOrm.sql`(
156
+ ${delivery.id}::text,
157
+ ${row.id}::text,
158
+ ${delivery.channel}::text,
159
+ ${delivery.maxAttempts}::integer,
160
+ ${ts(delivery.notBefore)}::timestamptz,
161
+ ${ts(now)}::timestamptz
162
+ )`
163
+ )
164
+ );
165
+ if (deliveries.length > 0) {
166
+ await tx.execute(drizzleOrm.sql`
167
+ INSERT INTO ${DELIVERY} (id, notification_id, channel, max_attempts, not_before, updated_at)
168
+ VALUES ${drizzleOrm.sql.join(deliveries, drizzleOrm.sql`, `)}
169
+ `);
170
+ }
171
+ return {
172
+ created: [...created],
173
+ deduped: input.filter((row) => !created.has(row.id)).map((row) => row.id)
174
+ };
175
+ });
176
+ },
177
+ async claimPendingDeliveries(args) {
178
+ const now = args.now ?? /* @__PURE__ */ new Date();
179
+ const staleBefore = new Date(now.getTime() - args.leaseMs);
180
+ const channelFilter = args.channels && args.channels.length > 0 ? drizzleOrm.sql`AND channel IN (${drizzleOrm.sql.join(
181
+ args.channels.map((channel) => drizzleOrm.sql`${channel}`),
182
+ drizzleOrm.sql`, `
183
+ )})` : drizzleOrm.sql``;
184
+ const idFilter = args.ids && args.ids.length > 0 ? drizzleOrm.sql`AND id IN (${drizzleOrm.sql.join(
185
+ args.ids.map((id) => drizzleOrm.sql`${id}`),
186
+ drizzleOrm.sql`, `
187
+ )})` : drizzleOrm.sql``;
188
+ const rows = await run(drizzleOrm.sql`
189
+ WITH claimed AS (
190
+ UPDATE ${DELIVERY}
191
+ SET status = 'claimed', claimed_at = ${ts(now)}::timestamptz, claimed_by = ${args.claimToken}::text
192
+ WHERE id IN (
193
+ SELECT id FROM ${DELIVERY}
194
+ WHERE (status = 'pending' OR (status = 'claimed' AND claimed_at < ${ts(staleBefore)}::timestamptz))
195
+ AND not_before <= ${ts(now)}::timestamptz
196
+ AND attempts < max_attempts
197
+ ${channelFilter}
198
+ ${idFilter}
199
+ ORDER BY not_before ASC, id ASC
200
+ LIMIT ${args.limit}
201
+ FOR UPDATE SKIP LOCKED
202
+ )
203
+ RETURNING id, notification_id, channel, attempts, max_attempts
204
+ )
205
+ SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts,
206
+ n.user_id, n.type, n.payload, n.actor_id
207
+ FROM claimed c
208
+ JOIN ${NOTIFICATION} n ON n.id = c.notification_id
209
+ `);
210
+ return rows.map((row) => ({
211
+ id: str(row.id),
212
+ notificationId: str(row.notification_id),
213
+ channel: str(row.channel),
214
+ attempts: num(row.attempts),
215
+ maxAttempts: num(row.max_attempts),
216
+ notification: {
217
+ userId: str(row.user_id),
218
+ type: str(row.type),
219
+ payload: row.payload,
220
+ actorId: nullableStr(row.actor_id)
221
+ }
222
+ }));
223
+ },
224
+ async releaseDeliveries(releases) {
225
+ if (releases.length === 0) return;
226
+ const now = /* @__PURE__ */ new Date();
227
+ const values = releases.map(({ id, outcome, nextAttemptAt }) => {
228
+ const retryable = outcome.result === "failed" && outcome.retryable;
229
+ const error = outcome.result === "failed" ? outcome.error.slice(0, 2e3) : null;
230
+ return drizzleOrm.sql`(
231
+ ${id}::text,
232
+ ${outcome.result}::text,
233
+ ${retryable}::boolean,
234
+ ${error}::text,
235
+ ${nextAttemptAt ? ts(nextAttemptAt) : null}::timestamptz
236
+ )`;
237
+ });
238
+ await db.execute(drizzleOrm.sql`
239
+ UPDATE ${DELIVERY} d
240
+ SET status = CASE
241
+ WHEN v.result = 'sent' THEN 'sent'
242
+ WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending'
243
+ ELSE 'failed'
244
+ END,
245
+ attempts = CASE WHEN v.result = 'sent' THEN d.attempts ELSE d.attempts + 1 END,
246
+ last_error = v.error,
247
+ not_before = COALESCE(v.not_before, d.not_before),
248
+ claimed_at = NULL,
249
+ claimed_by = NULL,
250
+ updated_at = ${ts(now)}::timestamptz
251
+ FROM (VALUES ${drizzleOrm.sql.join(values, drizzleOrm.sql`, `)}) AS v(id, result, retryable, error, not_before)
252
+ WHERE d.id = v.id
253
+ `);
254
+ },
255
+ async listNotifications(query) {
256
+ const cursor = query.cursor ? decodeCursor(query.cursor) : null;
257
+ const rows = await run(drizzleOrm.sql`
258
+ SELECT * FROM ${NOTIFICATION}
259
+ WHERE user_id = ${query.userId}::text
260
+ AND archived_at IS NULL
261
+ ${query.unreadOnly ? drizzleOrm.sql`AND read_at IS NULL` : drizzleOrm.sql``}
262
+ ${cursor ? drizzleOrm.sql`AND (created_at, id) < (${ts(cursor.createdAt)}::timestamptz, ${cursor.id}::text)` : drizzleOrm.sql``}
263
+ ORDER BY created_at DESC, id DESC
264
+ LIMIT ${query.limit + 1}
265
+ `);
266
+ const page = rows.slice(0, query.limit).map(toNotification);
267
+ const last = page.at(-1);
268
+ return {
269
+ notifications: page,
270
+ nextCursor: rows.length > query.limit && last ? encodeCursor(last.createdAt, last.id) : null
271
+ };
272
+ },
273
+ async countUnseen(userId) {
274
+ const rows = await run(drizzleOrm.sql`
275
+ SELECT count(*)::int AS count FROM ${NOTIFICATION}
276
+ WHERE user_id = ${userId}::text AND seen_at IS NULL AND archived_at IS NULL
277
+ `);
278
+ return num(rows[0]?.count ?? 0);
279
+ },
280
+ async markSeen(userId, before) {
281
+ await db.execute(drizzleOrm.sql`
282
+ UPDATE ${NOTIFICATION} SET seen_at = ${ts(/* @__PURE__ */ new Date())}::timestamptz
283
+ WHERE user_id = ${userId}::text AND seen_at IS NULL AND created_at <= ${ts(before)}::timestamptz
284
+ `);
285
+ },
286
+ async markRead(userId, notificationIds) {
287
+ if (notificationIds.length === 0) return 0;
288
+ const rows = await run(drizzleOrm.sql`
289
+ UPDATE ${NOTIFICATION}
290
+ SET read_at = COALESCE(read_at, ${ts(/* @__PURE__ */ new Date())}::timestamptz)
291
+ WHERE user_id = ${userId}::text
292
+ AND id IN (${drizzleOrm.sql.join(
293
+ notificationIds.map((id) => drizzleOrm.sql`${id}`),
294
+ drizzleOrm.sql`, `
295
+ )})
296
+ RETURNING id
297
+ `);
298
+ return rows.length;
299
+ },
300
+ async markAllRead(userId) {
301
+ const rows = await run(drizzleOrm.sql`
302
+ UPDATE ${NOTIFICATION} SET read_at = ${ts(/* @__PURE__ */ new Date())}::timestamptz
303
+ WHERE user_id = ${userId}::text AND read_at IS NULL
304
+ RETURNING id
305
+ `);
306
+ return rows.length;
307
+ },
308
+ async queryTable(table, where, options2) {
309
+ const order = options2.orderBy ? drizzleOrm.sql` ORDER BY ${drizzleOrm.sql.identifier(toSnakeCase(options2.orderBy.field))} ${options2.orderBy.direction === "desc" ? drizzleOrm.sql`DESC` : drizzleOrm.sql`ASC`}` : drizzleOrm.sql``;
310
+ const limit = options2.limit ? drizzleOrm.sql` LIMIT ${options2.limit}` : drizzleOrm.sql``;
311
+ return run(
312
+ drizzleOrm.sql`SELECT * FROM ${drizzleOrm.sql.identifier(table)}${whereSql(where)}${order}${limit}`
313
+ );
314
+ },
315
+ async insertRows(table, rows, onConflict) {
316
+ if (rows.length === 0) return 0;
317
+ const columns = Object.keys(rows[0] ?? {});
318
+ if (columns.length === 0) return 0;
319
+ const values = rows.map(
320
+ (row) => drizzleOrm.sql`(${drizzleOrm.sql.join(
321
+ columns.map((column) => drizzleOrm.sql`${bind(row[column])}`),
322
+ drizzleOrm.sql`, `
323
+ )})`
324
+ );
325
+ const conflict = onConflict && onConflict.length > 0 ? drizzleOrm.sql` ON CONFLICT (${drizzleOrm.sql.join(
326
+ onConflict.map((field) => drizzleOrm.sql.identifier(toSnakeCase(field))),
327
+ drizzleOrm.sql`, `
328
+ )}) DO UPDATE SET ${drizzleOrm.sql.join(
329
+ columns.filter((column) => !onConflict.includes(column)).map(
330
+ (column) => drizzleOrm.sql`${drizzleOrm.sql.identifier(toSnakeCase(column))} = EXCLUDED.${drizzleOrm.sql.identifier(
331
+ toSnakeCase(column)
332
+ )}`
333
+ ),
334
+ drizzleOrm.sql`, `
335
+ )}` : drizzleOrm.sql``;
336
+ const inserted = await run(drizzleOrm.sql`
337
+ INSERT INTO ${drizzleOrm.sql.identifier(table)} (${drizzleOrm.sql.join(
338
+ columns.map((column) => drizzleOrm.sql.identifier(toSnakeCase(column))),
339
+ drizzleOrm.sql`, `
340
+ )})
341
+ VALUES ${drizzleOrm.sql.join(values, drizzleOrm.sql`, `)}${conflict}
342
+ RETURNING 1 AS ok
343
+ `);
344
+ return inserted.length;
345
+ },
346
+ async updateRows(table, where, set) {
347
+ const assignments = Object.entries(set).map(
348
+ ([field, value]) => drizzleOrm.sql`${drizzleOrm.sql.identifier(toSnakeCase(field))} = ${bind(value)}`
349
+ );
350
+ if (assignments.length === 0) return 0;
351
+ const rows = await run(drizzleOrm.sql`
352
+ UPDATE ${drizzleOrm.sql.identifier(table)}
353
+ SET ${drizzleOrm.sql.join(assignments, drizzleOrm.sql`, `)}${whereSql(where)}
354
+ RETURNING 1 AS ok
355
+ `);
356
+ return rows.length;
357
+ },
358
+ async deleteRows(table, where) {
359
+ const rows = await run(
360
+ drizzleOrm.sql`DELETE FROM ${drizzleOrm.sql.identifier(table)}${whereSql(where)} RETURNING 1 AS ok`
361
+ );
362
+ return rows.length;
363
+ },
364
+ async getFailedDeliveries(args) {
365
+ const rows = await run(drizzleOrm.sql`
366
+ SELECT * FROM ${DELIVERY}
367
+ WHERE status = 'failed' AND updated_at >= ${ts(args.since)}::timestamptz
368
+ ORDER BY updated_at DESC
369
+ LIMIT ${args.limit}
370
+ `);
371
+ return rows.map(toDelivery);
372
+ }
373
+ };
374
+ }
375
+ function createSchema(prefix = "") {
376
+ const name = (base) => `${prefix}${base}`;
377
+ const notification = pgCore.pgTable(
378
+ name("notification"),
379
+ {
380
+ id: pgCore.text("id").primaryKey(),
381
+ userId: pgCore.text("user_id").notNull(),
382
+ type: pgCore.text("type").notNull(),
383
+ payload: pgCore.jsonb("payload").notNull(),
384
+ actorId: pgCore.text("actor_id"),
385
+ groupKey: pgCore.text("group_key"),
386
+ dedupeKey: pgCore.text("dedupe_key"),
387
+ seenAt: pgCore.timestamp("seen_at", { withTimezone: true }),
388
+ readAt: pgCore.timestamp("read_at", { withTimezone: true }),
389
+ archivedAt: pgCore.timestamp("archived_at", { withTimezone: true }),
390
+ createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
391
+ },
392
+ (table) => [
393
+ pgCore.index(name("notification_feed_idx")).on(table.userId, table.createdAt.desc()),
394
+ pgCore.index(name("notification_unseen_idx")).on(table.userId).where(drizzleOrm.sql`${table.seenAt} is null`),
395
+ pgCore.index(name("notification_group_idx")).on(table.userId, table.groupKey),
396
+ // Postgres treats NULLs as distinct here, so unlimited rows may carry no
397
+ // dedupe key while a present one is enforced unique per user.
398
+ pgCore.uniqueIndex(name("notification_dedupe_idx")).on(table.userId, table.dedupeKey)
399
+ ]
400
+ );
401
+ const notificationDelivery = pgCore.pgTable(
402
+ name("notification_delivery"),
403
+ {
404
+ id: pgCore.text("id").primaryKey(),
405
+ notificationId: pgCore.text("notification_id").notNull().references(() => notification.id, { onDelete: "cascade" }),
406
+ channel: pgCore.text("channel").$type().notNull(),
407
+ status: pgCore.text("status").$type().notNull().default("pending"),
408
+ attempts: pgCore.integer("attempts").notNull().default(0),
409
+ maxAttempts: pgCore.integer("max_attempts").notNull().default(5),
410
+ notBefore: pgCore.timestamp("not_before", { withTimezone: true }).notNull().defaultNow(),
411
+ claimedAt: pgCore.timestamp("claimed_at", { withTimezone: true }),
412
+ claimedBy: pgCore.text("claimed_by"),
413
+ lastError: pgCore.text("last_error"),
414
+ updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
415
+ },
416
+ (table) => [
417
+ pgCore.index(name("delivery_claim_idx")).on(table.status, table.notBefore),
418
+ pgCore.index(name("delivery_notification_idx")).on(table.notificationId)
419
+ ]
420
+ );
421
+ const notificationPreference = pgCore.pgTable(
422
+ name("notification_preference"),
423
+ {
424
+ userId: pgCore.text("user_id").notNull(),
425
+ type: pgCore.text("type").notNull(),
426
+ channel: pgCore.text("channel").$type().notNull(),
427
+ enabled: pgCore.boolean("enabled").notNull().default(true),
428
+ frequency: pgCore.text("frequency").$type().notNull().default("instant")
429
+ },
430
+ (table) => [pgCore.primaryKey({ columns: [table.userId, table.type, table.channel] })]
431
+ );
432
+ return { notification, notificationDelivery, notificationPreference };
433
+ }
434
+ var schema = createSchema();
435
+
436
+ exports.createSchema = createSchema;
437
+ exports.drizzleAdapter = drizzleAdapter;
438
+ exports.schema = schema;
439
+ //# sourceMappingURL=drizzle.cjs.map
440
+ //# sourceMappingURL=drizzle.cjs.map