easy-ping 0.1.0 → 0.2.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/README.md +44 -2
- package/dist/adapters/postgres.cjs +352 -0
- package/dist/adapters/postgres.cjs.map +1 -0
- package/dist/adapters/postgres.d.cts +38 -0
- package/dist/adapters/postgres.d.ts +38 -0
- package/dist/adapters/postgres.js +327 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/package.json +15 -11
package/README.md
CHANGED
|
@@ -43,6 +43,47 @@ import { coreSchema, renderPostgresDdl } from "easy-ping/schema";
|
|
|
43
43
|
for (const statement of renderPostgresDdl(coreSchema)) await sql.unsafe(statement);
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
<details>
|
|
47
|
+
<summary>Without an ORM (plain pg, postgres.js, Kysely…)</summary>
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add easy-ping pg zod
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { postgresAdapter } from "easy-ping/adapters/postgres";
|
|
55
|
+
import { Pool } from "pg";
|
|
56
|
+
|
|
57
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
58
|
+
|
|
59
|
+
const database = postgresAdapter(
|
|
60
|
+
async (text, params) => (await pool.query(text, params as unknown[])).rows,
|
|
61
|
+
{
|
|
62
|
+
// Optional, but it is what makes createNotifications atomic.
|
|
63
|
+
transaction: async (fn) => {
|
|
64
|
+
const client = await pool.connect();
|
|
65
|
+
try {
|
|
66
|
+
await client.query("BEGIN");
|
|
67
|
+
const result = await fn(async (t, p) => (await client.query(t, p as unknown[])).rows);
|
|
68
|
+
await client.query("COMMIT");
|
|
69
|
+
return result;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await client.query("ROLLBACK");
|
|
72
|
+
throw error;
|
|
73
|
+
} finally {
|
|
74
|
+
client.release();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
One function — run a parameterised statement, return rows — is the entire contract.
|
|
82
|
+
Anything that can do that works: `pg`, `postgres.js`, Kysely, Neon or PlanetScale's
|
|
83
|
+
serverless drivers, or Prisma's `$queryRawUnsafe`.
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
46
87
|
<details>
|
|
47
88
|
<summary>On MongoDB instead</summary>
|
|
48
89
|
|
|
@@ -284,8 +325,9 @@ Defaults to the last 24 hours, capped at 1000 rows. Wire it to an admin page or
|
|
|
284
325
|
| | |
|
|
285
326
|
| --- | --- |
|
|
286
327
|
| ✅ Core `send()` pipeline, hooks, dedupe | |
|
|
287
|
-
| ✅ Postgres
|
|
288
|
-
| ✅
|
|
328
|
+
| ✅ Postgres through any driver — no ORM needed | `pg`, `postgres.js`, Kysely, Neon… |
|
|
329
|
+
| ✅ Postgres via Drizzle, for those already on it | same conformance suite |
|
|
330
|
+
| ✅ MongoDB | same conformance suite |
|
|
289
331
|
| ✅ Delivery runner, all four modes, retry + backoff | |
|
|
290
332
|
| ✅ Resend provider | |
|
|
291
333
|
| ✅ Route handler, session scoping, cron | |
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/base64url.ts
|
|
4
|
+
function encodeBase64Url(input) {
|
|
5
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
6
|
+
let binary = "";
|
|
7
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
8
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function decodeBase64UrlBytes(value) {
|
|
11
|
+
try {
|
|
12
|
+
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
13
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function decodeBase64Url(value) {
|
|
19
|
+
const bytes = decodeBase64UrlBytes(value);
|
|
20
|
+
return bytes ? new TextDecoder().decode(bytes) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/schema/declaration.ts
|
|
24
|
+
var toSnakeCase = (value) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
25
|
+
|
|
26
|
+
// src/core/store.ts
|
|
27
|
+
var OPERATORS = ["in", "lt", "lte", "gt", "gte", "not"];
|
|
28
|
+
var isOperator = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && OPERATORS.some((op) => op in value);
|
|
29
|
+
|
|
30
|
+
// src/adapters/postgres/statement.ts
|
|
31
|
+
var Statement = class {
|
|
32
|
+
#text = "";
|
|
33
|
+
params = [];
|
|
34
|
+
/** Raw SQL. Never pass user input through this — use `value`. */
|
|
35
|
+
raw(fragment) {
|
|
36
|
+
this.#text += fragment;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/** Binds a value and writes its placeholder. */
|
|
40
|
+
value(value) {
|
|
41
|
+
this.params.push(value instanceof Date ? value.toISOString() : value);
|
|
42
|
+
this.#text += `$${this.params.length}`;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/** `$1, $2, $3` — a comma-separated run of bound values. */
|
|
46
|
+
list(values, separator = ", ") {
|
|
47
|
+
values.forEach((value, index) => {
|
|
48
|
+
if (index > 0) this.raw(separator);
|
|
49
|
+
this.value(value);
|
|
50
|
+
});
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/** `($1::text, $2::timestamptz)` — one VALUES tuple with explicit casts. */
|
|
54
|
+
tuple(cells) {
|
|
55
|
+
this.raw("(");
|
|
56
|
+
cells.forEach(([value, cast], index) => {
|
|
57
|
+
if (index > 0) this.raw(", ");
|
|
58
|
+
this.value(value).raw(`::${cast}`);
|
|
59
|
+
});
|
|
60
|
+
return this.raw(")");
|
|
61
|
+
}
|
|
62
|
+
get text() {
|
|
63
|
+
return this.#text;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var quote = (identifier) => `"${identifier.replace(/"/g, '""')}"`;
|
|
67
|
+
|
|
68
|
+
// src/adapters/postgres/adapter.ts
|
|
69
|
+
var str = (value) => String(value);
|
|
70
|
+
var nullableStr = (value) => value == null ? null : String(value);
|
|
71
|
+
var num = (value) => Number(value);
|
|
72
|
+
var date = (value) => value instanceof Date ? value : new Date(String(value));
|
|
73
|
+
var nullableDate = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
|
|
74
|
+
var encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
|
|
75
|
+
function decodeCursor(cursor) {
|
|
76
|
+
const decoded = decodeBase64Url(cursor);
|
|
77
|
+
if (!decoded) return null;
|
|
78
|
+
const [iso, id] = decoded.split("|");
|
|
79
|
+
if (!iso || !id) return null;
|
|
80
|
+
const createdAt = new Date(iso);
|
|
81
|
+
return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
|
|
82
|
+
}
|
|
83
|
+
function toNotification(row) {
|
|
84
|
+
return {
|
|
85
|
+
id: str(row.id),
|
|
86
|
+
userId: str(row.user_id),
|
|
87
|
+
type: str(row.type),
|
|
88
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
89
|
+
actorId: nullableStr(row.actor_id),
|
|
90
|
+
groupKey: nullableStr(row.group_key),
|
|
91
|
+
dedupeKey: nullableStr(row.dedupe_key),
|
|
92
|
+
seenAt: nullableDate(row.seen_at),
|
|
93
|
+
readAt: nullableDate(row.read_at),
|
|
94
|
+
archivedAt: nullableDate(row.archived_at),
|
|
95
|
+
createdAt: date(row.created_at)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function toDelivery(row) {
|
|
99
|
+
return {
|
|
100
|
+
id: str(row.id),
|
|
101
|
+
notificationId: str(row.notification_id),
|
|
102
|
+
channel: str(row.channel),
|
|
103
|
+
status: str(row.status),
|
|
104
|
+
attempts: num(row.attempts),
|
|
105
|
+
maxAttempts: num(row.max_attempts),
|
|
106
|
+
notBefore: date(row.not_before),
|
|
107
|
+
claimedAt: nullableDate(row.claimed_at),
|
|
108
|
+
claimedBy: nullableStr(row.claimed_by),
|
|
109
|
+
lastError: nullableStr(row.last_error),
|
|
110
|
+
updatedAt: date(row.updated_at)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function appendWhere(statement, where) {
|
|
114
|
+
const entries = Object.entries(where);
|
|
115
|
+
if (entries.length === 0) return;
|
|
116
|
+
statement.raw(" WHERE ");
|
|
117
|
+
entries.forEach(([field, condition], index) => {
|
|
118
|
+
if (index > 0) statement.raw(" AND ");
|
|
119
|
+
const column = quote(toSnakeCase(field));
|
|
120
|
+
if (condition === null) {
|
|
121
|
+
statement.raw(`${column} IS NULL`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!isOperator(condition)) {
|
|
125
|
+
statement.raw(`${column} = `).value(condition);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const operator = condition;
|
|
129
|
+
if ("in" in operator) {
|
|
130
|
+
const list = operator.in;
|
|
131
|
+
if (list.length === 0) statement.raw("FALSE");
|
|
132
|
+
else statement.raw(`${column} IN (`).list(list).raw(")");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if ("lt" in operator) statement.raw(`${column} < `).value(operator.lt);
|
|
136
|
+
else if ("lte" in operator) statement.raw(`${column} <= `).value(operator.lte);
|
|
137
|
+
else if ("gt" in operator) statement.raw(`${column} > `).value(operator.gt);
|
|
138
|
+
else if ("gte" in operator) statement.raw(`${column} >= `).value(operator.gte);
|
|
139
|
+
else if ("not" in operator) {
|
|
140
|
+
if (operator.not === null) statement.raw(`${column} IS NOT NULL`);
|
|
141
|
+
else statement.raw(`${column} IS DISTINCT FROM `).value(operator.not);
|
|
142
|
+
} else statement.raw("TRUE");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function postgresAdapter(query, options = {}) {
|
|
146
|
+
const prefix = options.prefix ?? "";
|
|
147
|
+
const NOTIFICATION = quote(`${prefix}notification`);
|
|
148
|
+
const DELIVERY = quote(`${prefix}notification_delivery`);
|
|
149
|
+
const run = async (statement, exec = query) => exec(statement.text, statement.params);
|
|
150
|
+
const atomically = (fn) => options.transaction ? options.transaction(fn) : fn(query);
|
|
151
|
+
return {
|
|
152
|
+
name: "postgres",
|
|
153
|
+
naming: "snake_case",
|
|
154
|
+
serializesJson: true,
|
|
155
|
+
async createNotifications(input) {
|
|
156
|
+
if (input.length === 0) return { created: [], deduped: [] };
|
|
157
|
+
const now = /* @__PURE__ */ new Date();
|
|
158
|
+
return atomically(async (exec) => {
|
|
159
|
+
const insert = new Statement().raw(
|
|
160
|
+
`INSERT INTO ${NOTIFICATION} (id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES `
|
|
161
|
+
);
|
|
162
|
+
input.forEach((row, index) => {
|
|
163
|
+
if (index > 0) insert.raw(", ");
|
|
164
|
+
insert.tuple([
|
|
165
|
+
[row.id, "text"],
|
|
166
|
+
[row.userId, "text"],
|
|
167
|
+
[row.type, "text"],
|
|
168
|
+
[JSON.stringify(row.payload ?? null), "jsonb"],
|
|
169
|
+
[row.actorId ?? null, "text"],
|
|
170
|
+
[row.groupKey ?? null, "text"],
|
|
171
|
+
[row.dedupeKey ?? null, "text"],
|
|
172
|
+
[now, "timestamptz"]
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
insert.raw(" ON CONFLICT (user_id, dedupe_key) DO NOTHING RETURNING id");
|
|
176
|
+
const inserted = await run(insert, exec);
|
|
177
|
+
const created = new Set(inserted.map((row) => str(row.id)));
|
|
178
|
+
const deliveries = input.filter((row) => created.has(row.id)).flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));
|
|
179
|
+
if (deliveries.length > 0) {
|
|
180
|
+
const insertDeliveries = new Statement().raw(
|
|
181
|
+
`INSERT INTO ${DELIVERY} (id, notification_id, channel, max_attempts, not_before, updated_at) VALUES `
|
|
182
|
+
);
|
|
183
|
+
deliveries.forEach(({ row, delivery }, index) => {
|
|
184
|
+
if (index > 0) insertDeliveries.raw(", ");
|
|
185
|
+
insertDeliveries.tuple([
|
|
186
|
+
[delivery.id, "text"],
|
|
187
|
+
[row.id, "text"],
|
|
188
|
+
[delivery.channel, "text"],
|
|
189
|
+
[delivery.maxAttempts, "integer"],
|
|
190
|
+
[delivery.notBefore, "timestamptz"],
|
|
191
|
+
[now, "timestamptz"]
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
await run(insertDeliveries, exec);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
created: [...created],
|
|
198
|
+
deduped: input.filter((row) => !created.has(row.id)).map((row) => row.id)
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
async claimPendingDeliveries(args) {
|
|
203
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
204
|
+
const staleBefore = new Date(now.getTime() - args.leaseMs);
|
|
205
|
+
const statement = new Statement().raw(`WITH claimed AS (UPDATE ${DELIVERY} SET status = 'claimed', claimed_at = `).value(now).raw("::timestamptz, claimed_by = ").value(args.claimToken).raw(`::text WHERE id IN (SELECT id FROM ${DELIVERY} WHERE (status = 'pending' OR `).raw("(status = 'claimed' AND claimed_at < ").value(staleBefore).raw("::timestamptz)) AND not_before <= ").value(now).raw("::timestamptz AND attempts < max_attempts");
|
|
206
|
+
if (args.channels && args.channels.length > 0) {
|
|
207
|
+
statement.raw(" AND channel IN (").list(args.channels).raw(")");
|
|
208
|
+
}
|
|
209
|
+
if (args.ids && args.ids.length > 0) {
|
|
210
|
+
statement.raw(" AND id IN (").list(args.ids).raw(")");
|
|
211
|
+
}
|
|
212
|
+
statement.raw(" ORDER BY not_before ASC, id ASC LIMIT ").value(args.limit).raw(" FOR UPDATE SKIP LOCKED)").raw(" RETURNING id, notification_id, channel, attempts, max_attempts)").raw(
|
|
213
|
+
` SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts, n.user_id, n.type, n.payload, n.actor_id FROM claimed c JOIN ${NOTIFICATION} n ON n.id = c.notification_id`
|
|
214
|
+
);
|
|
215
|
+
const rows = await run(statement);
|
|
216
|
+
return rows.map((row) => ({
|
|
217
|
+
id: str(row.id),
|
|
218
|
+
notificationId: str(row.notification_id),
|
|
219
|
+
channel: str(row.channel),
|
|
220
|
+
attempts: num(row.attempts),
|
|
221
|
+
maxAttempts: num(row.max_attempts),
|
|
222
|
+
notification: {
|
|
223
|
+
userId: str(row.user_id),
|
|
224
|
+
type: str(row.type),
|
|
225
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
226
|
+
actorId: nullableStr(row.actor_id)
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
async releaseDeliveries(releases) {
|
|
231
|
+
if (releases.length === 0) return;
|
|
232
|
+
const now = /* @__PURE__ */ new Date();
|
|
233
|
+
const statement = new Statement().raw(
|
|
234
|
+
`UPDATE ${DELIVERY} d SET status = CASE WHEN v.result = 'sent' THEN 'sent' WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending' ELSE 'failed' END, attempts = CASE WHEN v.result = 'sent' THEN d.attempts ELSE d.attempts + 1 END, last_error = v.error, not_before = COALESCE(v.not_before, d.not_before), claimed_at = NULL, claimed_by = NULL, updated_at = `
|
|
235
|
+
);
|
|
236
|
+
statement.value(now).raw("::timestamptz FROM (VALUES ");
|
|
237
|
+
releases.forEach(({ id, outcome, nextAttemptAt }, index) => {
|
|
238
|
+
if (index > 0) statement.raw(", ");
|
|
239
|
+
statement.tuple([
|
|
240
|
+
[id, "text"],
|
|
241
|
+
[outcome.result, "text"],
|
|
242
|
+
[outcome.result === "failed" && outcome.retryable, "boolean"],
|
|
243
|
+
[outcome.result === "failed" ? outcome.error.slice(0, 2e3) : null, "text"],
|
|
244
|
+
[nextAttemptAt ?? null, "timestamptz"]
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
statement.raw(") AS v(id, result, retryable, error, not_before) WHERE d.id = v.id");
|
|
248
|
+
await run(statement);
|
|
249
|
+
},
|
|
250
|
+
async listNotifications(feed) {
|
|
251
|
+
const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;
|
|
252
|
+
const statement = new Statement().raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `).value(feed.userId).raw("::text AND archived_at IS NULL");
|
|
253
|
+
if (feed.unreadOnly) statement.raw(" AND read_at IS NULL");
|
|
254
|
+
if (cursor) {
|
|
255
|
+
statement.raw(" AND (created_at, id) < (").value(cursor.createdAt).raw("::timestamptz, ").value(cursor.id).raw("::text)");
|
|
256
|
+
}
|
|
257
|
+
statement.raw(" ORDER BY created_at DESC, id DESC LIMIT ").value(feed.limit + 1);
|
|
258
|
+
const rows = await run(statement);
|
|
259
|
+
const page = rows.slice(0, feed.limit).map(toNotification);
|
|
260
|
+
const last = page.at(-1);
|
|
261
|
+
return {
|
|
262
|
+
notifications: page,
|
|
263
|
+
nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
async countUnseen(userId) {
|
|
267
|
+
const rows = await run(
|
|
268
|
+
new Statement().raw(`SELECT count(*)::int AS count FROM ${NOTIFICATION} WHERE user_id = `).value(userId).raw("::text AND seen_at IS NULL AND archived_at IS NULL")
|
|
269
|
+
);
|
|
270
|
+
return num(rows[0]?.count ?? 0);
|
|
271
|
+
},
|
|
272
|
+
async markSeen(userId, before) {
|
|
273
|
+
await run(
|
|
274
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET seen_at = `).value(/* @__PURE__ */ new Date()).raw("::timestamptz WHERE user_id = ").value(userId).raw("::text AND seen_at IS NULL AND created_at <= ").value(before).raw("::timestamptz")
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
async markRead(userId, notificationIds) {
|
|
278
|
+
if (notificationIds.length === 0) return 0;
|
|
279
|
+
const rows = await run(
|
|
280
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET read_at = COALESCE(read_at, `).value(/* @__PURE__ */ new Date()).raw("::timestamptz) WHERE user_id = ").value(userId).raw("::text AND id IN (").list(notificationIds).raw(") RETURNING id")
|
|
281
|
+
);
|
|
282
|
+
return rows.length;
|
|
283
|
+
},
|
|
284
|
+
async markAllRead(userId) {
|
|
285
|
+
const rows = await run(
|
|
286
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET read_at = `).value(/* @__PURE__ */ new Date()).raw("::timestamptz WHERE user_id = ").value(userId).raw("::text AND read_at IS NULL RETURNING id")
|
|
287
|
+
);
|
|
288
|
+
return rows.length;
|
|
289
|
+
},
|
|
290
|
+
async getFailedDeliveries(args) {
|
|
291
|
+
const rows = await run(
|
|
292
|
+
new Statement().raw(`SELECT * FROM ${DELIVERY} WHERE status = 'failed' AND updated_at >= `).value(args.since).raw("::timestamptz ORDER BY updated_at DESC LIMIT ").value(args.limit)
|
|
293
|
+
);
|
|
294
|
+
return rows.map(toDelivery);
|
|
295
|
+
},
|
|
296
|
+
async queryTable(table, where, queryOptions) {
|
|
297
|
+
const statement = new Statement().raw(`SELECT * FROM ${quote(table)}`);
|
|
298
|
+
appendWhere(statement, where);
|
|
299
|
+
if (queryOptions.orderBy) {
|
|
300
|
+
const direction = queryOptions.orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
301
|
+
statement.raw(` ORDER BY ${quote(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);
|
|
302
|
+
}
|
|
303
|
+
if (queryOptions.limit) statement.raw(" LIMIT ").value(queryOptions.limit);
|
|
304
|
+
return [...await run(statement)];
|
|
305
|
+
},
|
|
306
|
+
async insertRows(table, rows, onConflict) {
|
|
307
|
+
if (rows.length === 0) return 0;
|
|
308
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
309
|
+
if (columns.length === 0) return 0;
|
|
310
|
+
const statement = new Statement().raw(
|
|
311
|
+
`INSERT INTO ${quote(table)} (${columns.map((c) => quote(toSnakeCase(c))).join(", ")}) VALUES `
|
|
312
|
+
);
|
|
313
|
+
rows.forEach((row, index) => {
|
|
314
|
+
if (index > 0) statement.raw(", ");
|
|
315
|
+
statement.raw("(").list(columns.map((column) => row[column])).raw(")");
|
|
316
|
+
});
|
|
317
|
+
if (onConflict && onConflict.length > 0) {
|
|
318
|
+
const assignments = columns.filter((column) => !onConflict.includes(column)).map((column) => {
|
|
319
|
+
const quoted = quote(toSnakeCase(column));
|
|
320
|
+
return `${quoted} = EXCLUDED.${quoted}`;
|
|
321
|
+
});
|
|
322
|
+
statement.raw(
|
|
323
|
+
` ON CONFLICT (${onConflict.map((f) => quote(toSnakeCase(f))).join(", ")}) DO UPDATE SET ${assignments.join(", ")}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
327
|
+
return (await run(statement)).length;
|
|
328
|
+
},
|
|
329
|
+
async updateRows(table, where, set) {
|
|
330
|
+
const assignments = Object.entries(set);
|
|
331
|
+
if (assignments.length === 0) return 0;
|
|
332
|
+
const statement = new Statement().raw(`UPDATE ${quote(table)} SET `);
|
|
333
|
+
assignments.forEach(([field, value], index) => {
|
|
334
|
+
if (index > 0) statement.raw(", ");
|
|
335
|
+
statement.raw(`${quote(toSnakeCase(field))} = `).value(value);
|
|
336
|
+
});
|
|
337
|
+
appendWhere(statement, where);
|
|
338
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
339
|
+
return (await run(statement)).length;
|
|
340
|
+
},
|
|
341
|
+
async deleteRows(table, where) {
|
|
342
|
+
const statement = new Statement().raw(`DELETE FROM ${quote(table)}`);
|
|
343
|
+
appendWhere(statement, where);
|
|
344
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
345
|
+
return (await run(statement)).length;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
exports.postgresAdapter = postgresAdapter;
|
|
351
|
+
//# sourceMappingURL=postgres.cjs.map
|
|
352
|
+
//# sourceMappingURL=postgres.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/base64url.ts","../../src/schema/declaration.ts","../../src/core/store.ts","../../src/adapters/postgres/statement.ts","../../src/adapters/postgres/adapter.ts"],"names":[],"mappings":"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","file":"postgres.cjs","sourcesContent":["// Web APIs, not Buffer: this runs on Workers and Vercel Edge.\n\nexport function encodeBase64Url(input: string | Uint8Array): string {\n const bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Uint8Array<ArrayBuffer>: bare Uint8Array widens and is not a BufferSource.\nexport function decodeBase64UrlBytes(value: string): Uint8Array<ArrayBuffer> | null {\n try {\n const binary = atob(value.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\nexport function decodeBase64Url(value: string): string | null {\n const bytes = decodeBase64UrlBytes(value);\n return bytes ? new TextDecoder().decode(bytes) : null;\n}\n","import type { SchemaDeclaration } from \"../core/plugin\";\n\nexport const toSnakeCase = (value: string) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n\n/**\n * Core tables in the format plugins use. Less expressive than the hand-written\n * Drizzle schema (no partial indexes, ordering or cascade) — a drift test\n * asserts the two agree on the fields they share.\n */\nexport const coreSchema = {\n notification: {\n tableName: \"notification\",\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n payload: { type: \"json\", required: true },\n actorId: { type: \"string\" },\n groupKey: { type: \"string\" },\n dedupeKey: { type: \"string\" },\n seenAt: { type: \"date\" },\n readAt: { type: \"date\" },\n archivedAt: { type: \"date\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\", \"createdAt\"], name: \"notification_feed_idx\" },\n { on: [\"userId\", \"groupKey\"], name: \"notification_group_idx\" },\n { on: [\"userId\", \"dedupeKey\"], unique: true, name: \"notification_dedupe_idx\" },\n ],\n },\n\n notificationDelivery: {\n tableName: \"notification_delivery\",\n fields: {\n id: { type: \"string\", required: true },\n notificationId: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n status: { type: \"string\", required: true, default: \"pending\" },\n attempts: { type: \"number\", required: true, default: 0 },\n maxAttempts: { type: \"number\", required: true, default: 5 },\n notBefore: { type: \"date\", required: true, defaultNow: true },\n claimedAt: { type: \"date\" },\n claimedBy: { type: \"string\" },\n lastError: { type: \"string\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"status\", \"notBefore\"], name: \"delivery_claim_idx\" },\n { on: [\"notificationId\"], name: \"delivery_notification_idx\" },\n ],\n },\n\n notificationPreference: {\n tableName: \"notification_preference\",\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n} satisfies SchemaDeclaration;\n","import { toSnakeCase } from \"../schema/declaration\";\nimport { ConfigError } from \"./errors\";\nimport type { SchemaDeclaration, TableDeclaration } from \"./plugin\";\n\nexport type Scalar = string | number | boolean | Date | null;\n\n/**\n * One operator per condition. Operators are tagged objects so a bare value is\n * always an equality test; json columns therefore cannot be filtered, since an\n * object value would be ambiguous.\n */\nexport type WhereCondition =\n | Scalar\n | { in: readonly (string | number)[] }\n | { lt: Date | number }\n | { lte: Date | number }\n | { gt: Date | number }\n | { gte: Date | number }\n | { not: Scalar };\n\nexport type WhereClause = Record<string, WhereCondition>;\n\nexport type QueryOptions = {\n limit?: number;\n orderBy?: { field: string; direction: \"asc\" | \"desc\" };\n};\n\nexport type UpsertOptions = { onConflict: readonly string[] };\n\n/**\n * Table access for plugins, scoped to the tables that plugin declared.\n *\n * Plugins could always declare tables via schema() but had no way to read\n * them, so preferences had to bolt its queries onto the core adapter. That\n * does not generalise to digests or push.\n */\nexport type PluginStore = {\n find<T = Record<string, unknown>>(\n table: string,\n where?: WhereClause,\n options?: QueryOptions,\n ): Promise<T[]>;\n insert(table: string, rows: readonly Record<string, unknown>[]): Promise<number>;\n upsert(\n table: string,\n rows: readonly Record<string, unknown>[],\n options: UpsertOptions,\n ): Promise<number>;\n update(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n remove(table: string, where: WhereClause): Promise<number>;\n};\n\nexport type TableStorage = {\n readonly naming?: \"snake_case\" | \"preserve\";\n readonly serializesJson?: boolean;\n\n queryTable(\n table: string,\n where: WhereClause,\n options: QueryOptions,\n ): Promise<Record<string, unknown>[]>;\n insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ): Promise<number>;\n updateRows(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n deleteRows(table: string, where: WhereClause): Promise<number>;\n};\n\nconst OPERATORS = [\"in\", \"lt\", \"lte\", \"gt\", \"gte\", \"not\"] as const;\n\nexport const isOperator = (value: unknown): boolean =>\n typeof value === \"object\" &&\n value !== null &&\n !(value instanceof Date) &&\n OPERATORS.some((op) => op in value);\n\n/**\n * Every table and column a plugin touches is checked against its own\n * declaration. Without this a plugin could read the notification table, and\n * unvalidated identifiers would reach the SQL builder.\n */\nexport function createPluginStore(\n pluginId: string,\n schema: SchemaDeclaration | undefined,\n storage: TableStorage,\n prefix: string,\n): PluginStore {\n const byTableName = new Map<string, TableDeclaration>();\n for (const declaration of Object.values(schema ?? {})) {\n byTableName.set(declaration.tableName, declaration);\n }\n\n function resolve(table: string): TableDeclaration {\n const declaration = byTableName.get(table);\n if (!declaration) {\n throw new ConfigError(\n `plugin \"${pluginId}\" accessed table \"${table}\", which it does not declare in schema().`,\n );\n }\n return declaration;\n }\n\n function checkFields(table: TableDeclaration, fields: Iterable<string>, context: string) {\n for (const field of fields) {\n const spec = table.fields[field];\n if (!spec) {\n throw new ConfigError(\n `plugin \"${pluginId}\" used unknown field \"${field}\" on \"${table.tableName}\" (${context}).`,\n );\n }\n if (context === \"where\" && spec.type === \"json\") {\n throw new ConfigError(\n `plugin \"${pluginId}\" cannot filter on json field \"${field}\"; ` +\n \"a bare object value is indistinguishable from an operator.\",\n );\n }\n }\n }\n\n const qualified = (table: TableDeclaration) => prefix + table.tableName;\n\n // A document store keeps the declared names; a SQL adapter wants columns.\n const column = (field: string) => (storage.naming === \"preserve\" ? field : toSnakeCase(field));\n\n /**\n * json columns must be handed to the driver as text; postgres-js cannot bind\n * a plain object and the insert fails outright.\n */\n const serialize = (table: TableDeclaration, row: Record<string, unknown>) => {\n if (storage.serializesJson === false) return row;\n\n const out: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(row)) {\n out[field] =\n table.fields[field]?.type === \"json\" && value !== null && value !== undefined\n ? JSON.stringify(value)\n : value;\n }\n return out;\n };\n\n return {\n async find(table, where = {}, options = {}) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n if (options.orderBy) checkFields(declaration, [options.orderBy.field], \"orderBy\");\n\n const rows = await storage.queryTable(qualified(declaration), where, options);\n\n // SELECT * returns snake_case columns; plugins declare camelCase fields\n // and type their reads that way. Without this every property is\n // undefined at runtime while typechecking perfectly.\n return rows.map((row) => {\n const mapped: Record<string, unknown> = {};\n for (const field of Object.keys(declaration.fields)) {\n const value = row[column(field)];\n // Drivers differ: some hand back parsed jsonb, some raw text.\n mapped[field] =\n declaration.fields[field]?.type === \"json\" && typeof value === \"string\"\n ? JSON.parse(value)\n : value;\n }\n return mapped;\n }) as never;\n },\n\n async insert(table, rows) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"insert\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n );\n },\n\n async upsert(table, rows, options) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"upsert\");\n checkFields(declaration, options.onConflict, \"onConflict\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n options.onConflict,\n );\n },\n\n async update(table, where, set) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n checkFields(declaration, Object.keys(set), \"update\");\n return storage.updateRows(qualified(declaration), where, serialize(declaration, set));\n },\n\n async remove(table, where) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n return storage.deleteRows(qualified(declaration), where);\n },\n };\n}\n","/**\n * Builds a parameterised statement: SQL text with `$1, $2 …` placeholders plus\n * the values to bind, which is the one calling convention every Postgres\n * driver agrees on.\n *\n * Values are never interpolated. Identifiers are quoted, and the only\n * identifiers that reach here are either constants in this file or names the\n * PluginStore has already validated against the plugin's own schema.\n */\nexport class Statement {\n #text = \"\";\n readonly params: unknown[] = [];\n\n /** Raw SQL. Never pass user input through this — use `value`. */\n raw(fragment: string): this {\n this.#text += fragment;\n return this;\n }\n\n /** Binds a value and writes its placeholder. */\n value(value: unknown): this {\n // Dates bind as ISO strings: every driver accepts that for timestamptz,\n // whereas a Date object is only handled by some of them.\n this.params.push(value instanceof Date ? value.toISOString() : value);\n this.#text += `$${this.params.length}`;\n return this;\n }\n\n /** `$1, $2, $3` — a comma-separated run of bound values. */\n list(values: readonly unknown[], separator = \", \"): this {\n values.forEach((value, index) => {\n if (index > 0) this.raw(separator);\n this.value(value);\n });\n return this;\n }\n\n /** `($1::text, $2::timestamptz)` — one VALUES tuple with explicit casts. */\n tuple(cells: readonly (readonly [unknown, string])[]): this {\n this.raw(\"(\");\n cells.forEach(([value, cast], index) => {\n if (index > 0) this.raw(\", \");\n this.value(value).raw(`::${cast}`);\n });\n return this.raw(\")\");\n }\n\n get text(): string {\n return this.#text;\n }\n}\n\n/** Double-quoted so a table or column name can never be read as SQL. */\nexport const quote = (identifier: string): string => `\"${identifier.replace(/\"/g, '\"\"')}\"`;\n","import type {\n ClaimArgs,\n ClaimedDelivery,\n DatabaseAdapter,\n DeliveryRelease,\n FeedPage,\n FeedQuery,\n InsertNotification,\n} from \"../../core/adapter\";\nimport { decodeBase64Url, encodeBase64Url } from \"../../core/base64url\";\nimport { isOperator, type QueryOptions, type WhereClause } from \"../../core/store\";\nimport type { Channel, DeliveryRecord, DeliveryStatus, NotificationRecord } from \"../../core/types\";\nimport { toSnakeCase } from \"../../schema/declaration\";\nimport { quote, Statement } from \"./statement\";\n\ntype Row = Record<string, unknown>;\n\n/**\n * Run one parameterised statement and return its rows.\n *\n * This is the whole contract — the lowest common denominator every Postgres\n * driver already exposes, which is what makes this adapter work with `pg`,\n * `postgres.js`, Kysely, Neon, or anything else without an ORM in between.\n */\nexport type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;\n\nexport type PostgresAdapterOptions = {\n /** Table-name prefix. Must match the instance's `tablePrefix`. */\n prefix?: string;\n /**\n * Runs `fn` inside a transaction, passing it a query function bound to that\n * transaction. Supply it and `createNotifications` becomes atomic — a\n * notification and its deliveries land together or neither does. Without it\n * the adapter still works; it just loses that guarantee, the same tradeoff\n * the Mongo adapter makes when no client is passed.\n */\n transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;\n};\n\nconst str = (value: unknown): string => String(value);\nconst nullableStr = (value: unknown): string | null => (value == null ? null : String(value));\nconst num = (value: unknown): number => Number(value);\nconst date = (value: unknown): Date => (value instanceof Date ? value : new Date(String(value)));\nconst nullableDate = (value: unknown): Date | null =>\n value == null ? null : value instanceof Date ? value : new Date(String(value));\n\nconst encodeCursor = (createdAt: Date, id: string) =>\n encodeBase64Url(`${createdAt.toISOString()}|${id}`);\n\nfunction decodeCursor(cursor: string): { createdAt: Date; id: string } | null {\n const decoded = decodeBase64Url(cursor);\n if (!decoded) return null;\n\n const [iso, id] = decoded.split(\"|\");\n if (!iso || !id) return null;\n\n const createdAt = new Date(iso);\n return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };\n}\n\nfunction toNotification(row: Row): NotificationRecord {\n return {\n id: str(row.id),\n userId: str(row.user_id),\n type: str(row.type),\n payload: typeof row.payload === \"string\" ? JSON.parse(row.payload) : row.payload,\n actorId: nullableStr(row.actor_id),\n groupKey: nullableStr(row.group_key),\n dedupeKey: nullableStr(row.dedupe_key),\n seenAt: nullableDate(row.seen_at),\n readAt: nullableDate(row.read_at),\n archivedAt: nullableDate(row.archived_at),\n createdAt: date(row.created_at),\n };\n}\n\nfunction toDelivery(row: Row): DeliveryRecord {\n return {\n id: str(row.id),\n notificationId: str(row.notification_id),\n channel: str(row.channel) as Channel,\n status: str(row.status) as DeliveryStatus,\n attempts: num(row.attempts),\n maxAttempts: num(row.max_attempts),\n notBefore: date(row.not_before),\n claimedAt: nullableDate(row.claimed_at),\n claimedBy: nullableStr(row.claimed_by),\n lastError: nullableStr(row.last_error),\n updatedAt: date(row.updated_at),\n };\n}\n\n/** Appends ` WHERE …` for a PluginStore clause. Values are always bound. */\nfunction appendWhere(statement: Statement, where: WhereClause): void {\n const entries = Object.entries(where);\n if (entries.length === 0) return;\n\n statement.raw(\" WHERE \");\n\n entries.forEach(([field, condition], index) => {\n if (index > 0) statement.raw(\" AND \");\n const column = quote(toSnakeCase(field));\n\n if (condition === null) {\n statement.raw(`${column} IS NULL`);\n return;\n }\n\n if (!isOperator(condition)) {\n statement.raw(`${column} = `).value(condition);\n return;\n }\n\n const operator = condition as Record<string, unknown>;\n\n if (\"in\" in operator) {\n const list = operator.in as readonly (string | number)[];\n // An empty IN () is a syntax error, and matching nothing is the honest\n // reading of \"in this empty set\".\n if (list.length === 0) statement.raw(\"FALSE\");\n else statement.raw(`${column} IN (`).list(list).raw(\")\");\n return;\n }\n\n if (\"lt\" in operator) statement.raw(`${column} < `).value(operator.lt);\n else if (\"lte\" in operator) statement.raw(`${column} <= `).value(operator.lte);\n else if (\"gt\" in operator) statement.raw(`${column} > `).value(operator.gt);\n else if (\"gte\" in operator) statement.raw(`${column} >= `).value(operator.gte);\n else if (\"not\" in operator) {\n if (operator.not === null) statement.raw(`${column} IS NOT NULL`);\n else statement.raw(`${column} IS DISTINCT FROM `).value(operator.not);\n } else statement.raw(\"TRUE\");\n });\n}\n\n/**\n * Postgres, through any driver — no ORM required.\n *\n * `drizzleAdapter` predates this and is kept for people already on Drizzle,\n * but it only ever used Drizzle as a SQL builder. This takes the query\n * function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a\n * serverless driver all work without pulling an ORM into the dependency tree.\n *\n * The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP\n * LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim\n * primitive depends on the second of those. See RFC 0003.\n */\nexport function postgresAdapter(\n query: SqlQuery,\n options: PostgresAdapterOptions = {},\n): DatabaseAdapter {\n const prefix = options.prefix ?? \"\";\n\n const NOTIFICATION = quote(`${prefix}notification`);\n const DELIVERY = quote(`${prefix}notification_delivery`);\n\n const run = async (statement: Statement, exec: SqlQuery = query) =>\n exec(statement.text, statement.params);\n\n /** Uses a transaction when one was supplied, and runs plainly otherwise. */\n const atomically = <T>(fn: (exec: SqlQuery) => Promise<T>): Promise<T> =>\n options.transaction ? options.transaction(fn) : fn(query);\n\n return {\n name: \"postgres\",\n naming: \"snake_case\",\n serializesJson: true,\n\n async createNotifications(input: readonly InsertNotification[]) {\n if (input.length === 0) return { created: [], deduped: [] };\n\n // One clock. created_at has a DEFAULT now() for hand-written SQL, but\n // now() is the *database* clock while every cutoff compared against it\n // (markSeen, getFailedDeliveries) comes from the app. In production those\n // are different hosts, and NTP skew put the newest rows outside markSeen.\n const now = new Date();\n\n return atomically(async (exec) => {\n const insert = new Statement().raw(\n `INSERT INTO ${NOTIFICATION} ` +\n \"(id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES \",\n );\n\n input.forEach((row, index) => {\n if (index > 0) insert.raw(\", \");\n insert.tuple([\n [row.id, \"text\"],\n [row.userId, \"text\"],\n [row.type, \"text\"],\n [JSON.stringify(row.payload ?? null), \"jsonb\"],\n [row.actorId ?? null, \"text\"],\n [row.groupKey ?? null, \"text\"],\n [row.dedupeKey ?? null, \"text\"],\n [now, \"timestamptz\"],\n ]);\n });\n\n // Rows with a NULL dedupe_key never conflict — Postgres treats NULLs as\n // distinct — so unlimited undeduped notifications coexist.\n insert.raw(\" ON CONFLICT (user_id, dedupe_key) DO NOTHING RETURNING id\");\n\n const inserted = await run(insert, exec);\n const created = new Set(inserted.map((row) => str(row.id)));\n\n const deliveries = input\n .filter((row) => created.has(row.id))\n .flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));\n\n if (deliveries.length > 0) {\n const insertDeliveries = new Statement().raw(\n `INSERT INTO ${DELIVERY} ` +\n \"(id, notification_id, channel, max_attempts, not_before, updated_at) VALUES \",\n );\n\n deliveries.forEach(({ row, delivery }, index) => {\n if (index > 0) insertDeliveries.raw(\", \");\n insertDeliveries.tuple([\n [delivery.id, \"text\"],\n [row.id, \"text\"],\n [delivery.channel, \"text\"],\n [delivery.maxAttempts, \"integer\"],\n [delivery.notBefore, \"timestamptz\"],\n [now, \"timestamptz\"],\n ]);\n });\n\n await run(insertDeliveries, exec);\n }\n\n return {\n created: [...created],\n deduped: input.filter((row) => !created.has(row.id)).map((row) => row.id),\n };\n });\n },\n\n async claimPendingDeliveries(args: ClaimArgs): Promise<readonly ClaimedDelivery[]> {\n const now = args.now ?? new Date();\n const staleBefore = new Date(now.getTime() - args.leaseMs);\n\n // SKIP LOCKED is what lets concurrent sweeps step around each other\n // rather than block. The CTE joins the notification in the same round\n // trip — claiming 20 rows then looking each one up is the N+1 this\n // primitive exists to avoid. See RFC 0003 §5.\n const statement = new Statement()\n .raw(`WITH claimed AS (UPDATE ${DELIVERY} SET status = 'claimed', claimed_at = `)\n .value(now)\n .raw(\"::timestamptz, claimed_by = \")\n .value(args.claimToken)\n .raw(`::text WHERE id IN (SELECT id FROM ${DELIVERY} WHERE (status = 'pending' OR `)\n .raw(\"(status = 'claimed' AND claimed_at < \")\n .value(staleBefore)\n .raw(\"::timestamptz)) AND not_before <= \")\n .value(now)\n .raw(\"::timestamptz AND attempts < max_attempts\");\n\n if (args.channels && args.channels.length > 0) {\n statement.raw(\" AND channel IN (\").list(args.channels).raw(\")\");\n }\n if (args.ids && args.ids.length > 0) {\n statement.raw(\" AND id IN (\").list(args.ids).raw(\")\");\n }\n\n statement\n .raw(\" ORDER BY not_before ASC, id ASC LIMIT \")\n .value(args.limit)\n .raw(\" FOR UPDATE SKIP LOCKED)\")\n .raw(\" RETURNING id, notification_id, channel, attempts, max_attempts)\")\n .raw(\n \" SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts,\" +\n \" n.user_id, n.type, n.payload, n.actor_id FROM claimed c\" +\n ` JOIN ${NOTIFICATION} n ON n.id = c.notification_id`,\n );\n\n const rows = await run(statement);\n\n return rows.map((row) => ({\n id: str(row.id),\n notificationId: str(row.notification_id),\n channel: str(row.channel) as Channel,\n attempts: num(row.attempts),\n maxAttempts: num(row.max_attempts),\n notification: {\n userId: str(row.user_id),\n type: str(row.type),\n payload: typeof row.payload === \"string\" ? JSON.parse(row.payload) : row.payload,\n actorId: nullableStr(row.actor_id),\n },\n }));\n },\n\n async releaseDeliveries(releases: readonly DeliveryRelease[]) {\n if (releases.length === 0) return;\n const now = new Date();\n\n // Written unconditionally — no claimed_by predicate. If the lease expired\n // and another worker re-sent, the duplicate already happened; recording\n // the true terminal state beats wedging the row in 'claimed'. RFC 0003 §6.\n const statement = new Statement().raw(\n `UPDATE ${DELIVERY} d SET status = CASE` +\n \" WHEN v.result = 'sent' THEN 'sent'\" +\n \" WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending'\" +\n \" ELSE 'failed' END,\" +\n \" attempts = CASE WHEN v.result = 'sent' THEN d.attempts ELSE d.attempts + 1 END,\" +\n \" last_error = v.error,\" +\n \" not_before = COALESCE(v.not_before, d.not_before),\" +\n \" claimed_at = NULL, claimed_by = NULL, updated_at = \",\n );\n\n statement.value(now).raw(\"::timestamptz FROM (VALUES \");\n\n releases.forEach(({ id, outcome, nextAttemptAt }, index) => {\n if (index > 0) statement.raw(\", \");\n statement.tuple([\n [id, \"text\"],\n [outcome.result, \"text\"],\n [outcome.result === \"failed\" && outcome.retryable, \"boolean\"],\n [outcome.result === \"failed\" ? outcome.error.slice(0, 2000) : null, \"text\"],\n [nextAttemptAt ?? null, \"timestamptz\"],\n ]);\n });\n\n statement.raw(\") AS v(id, result, retryable, error, not_before) WHERE d.id = v.id\");\n\n await run(statement);\n },\n\n async listNotifications(feed: FeedQuery): Promise<FeedPage> {\n const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;\n\n const statement = new Statement()\n .raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `)\n .value(feed.userId)\n .raw(\"::text AND archived_at IS NULL\");\n\n if (feed.unreadOnly) statement.raw(\" AND read_at IS NULL\");\n\n if (cursor) {\n statement\n .raw(\" AND (created_at, id) < (\")\n .value(cursor.createdAt)\n .raw(\"::timestamptz, \")\n .value(cursor.id)\n .raw(\"::text)\");\n }\n\n statement.raw(\" ORDER BY created_at DESC, id DESC LIMIT \").value(feed.limit + 1);\n\n const rows = await run(statement);\n const page = rows.slice(0, feed.limit).map(toNotification);\n const last = page.at(-1);\n\n return {\n notifications: page,\n nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null,\n };\n },\n\n async countUnseen(userId: string) {\n const rows = await run(\n new Statement()\n .raw(`SELECT count(*)::int AS count FROM ${NOTIFICATION} WHERE user_id = `)\n .value(userId)\n .raw(\"::text AND seen_at IS NULL AND archived_at IS NULL\"),\n );\n return num(rows[0]?.count ?? 0);\n },\n\n async markSeen(userId: string, before: Date) {\n await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET seen_at = `)\n .value(new Date())\n .raw(\"::timestamptz WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND seen_at IS NULL AND created_at <= \")\n .value(before)\n .raw(\"::timestamptz\"),\n );\n },\n\n async markRead(userId: string, notificationIds: readonly string[]) {\n if (notificationIds.length === 0) return 0;\n\n // Scoped by user_id as well as id — a caller must never be able to flip\n // someone else's row by guessing an id. RFC 0002 §2.\n // No `read_at IS NULL` filter: re-marking must be idempotent. Returning 0\n // for an already-read row made the route 404, which made the client roll\n // its optimistic update back and show the item as unread again.\n const rows = await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET read_at = COALESCE(read_at, `)\n .value(new Date())\n .raw(\"::timestamptz) WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND id IN (\")\n .list(notificationIds)\n .raw(\") RETURNING id\"),\n );\n return rows.length;\n },\n\n async markAllRead(userId: string) {\n const rows = await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET read_at = `)\n .value(new Date())\n .raw(\"::timestamptz WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND read_at IS NULL RETURNING id\"),\n );\n return rows.length;\n },\n\n async getFailedDeliveries(args: { since: Date; limit: number }) {\n const rows = await run(\n new Statement()\n .raw(`SELECT * FROM ${DELIVERY} WHERE status = 'failed' AND updated_at >= `)\n .value(args.since)\n .raw(\"::timestamptz ORDER BY updated_at DESC LIMIT \")\n .value(args.limit),\n );\n return rows.map(toDelivery);\n },\n\n async queryTable(table: string, where: WhereClause, queryOptions: QueryOptions) {\n const statement = new Statement().raw(`SELECT * FROM ${quote(table)}`);\n appendWhere(statement, where);\n\n if (queryOptions.orderBy) {\n const direction = queryOptions.orderBy.direction === \"desc\" ? \"DESC\" : \"ASC\";\n statement.raw(` ORDER BY ${quote(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);\n }\n if (queryOptions.limit) statement.raw(\" LIMIT \").value(queryOptions.limit);\n\n return [...(await run(statement))];\n },\n\n async insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ) {\n if (rows.length === 0) return 0;\n\n // Column order is taken from the first row and every row is projected\n // onto it, so a ragged batch cannot shift values into other columns.\n const columns = Object.keys(rows[0] ?? {});\n if (columns.length === 0) return 0;\n\n const statement = new Statement().raw(\n `INSERT INTO ${quote(table)} (${columns.map((c) => quote(toSnakeCase(c))).join(\", \")}) VALUES `,\n );\n\n rows.forEach((row, index) => {\n if (index > 0) statement.raw(\", \");\n statement\n .raw(\"(\")\n .list(columns.map((column) => row[column]))\n .raw(\")\");\n });\n\n if (onConflict && onConflict.length > 0) {\n const assignments = columns\n .filter((column) => !onConflict.includes(column))\n .map((column) => {\n const quoted = quote(toSnakeCase(column));\n return `${quoted} = EXCLUDED.${quoted}`;\n });\n\n statement.raw(\n ` ON CONFLICT (${onConflict.map((f) => quote(toSnakeCase(f))).join(\", \")}) DO UPDATE SET ${assignments.join(\", \")}`,\n );\n }\n\n statement.raw(\" RETURNING 1 AS ok\");\n return (await run(statement)).length;\n },\n\n async updateRows(table: string, where: WhereClause, set: Record<string, unknown>) {\n const assignments = Object.entries(set);\n if (assignments.length === 0) return 0;\n\n const statement = new Statement().raw(`UPDATE ${quote(table)} SET `);\n\n assignments.forEach(([field, value], index) => {\n if (index > 0) statement.raw(\", \");\n statement.raw(`${quote(toSnakeCase(field))} = `).value(value);\n });\n\n appendWhere(statement, where);\n statement.raw(\" RETURNING 1 AS ok\");\n\n return (await run(statement)).length;\n },\n\n async deleteRows(table: string, where: WhereClause) {\n const statement = new Statement().raw(`DELETE FROM ${quote(table)}`);\n appendWhere(statement, where);\n statement.raw(\" RETURNING 1 AS ok\");\n\n return (await run(statement)).length;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-f0OR2DhY.cjs';
|
|
2
|
+
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Run one parameterised statement and return its rows.
|
|
6
|
+
*
|
|
7
|
+
* This is the whole contract — the lowest common denominator every Postgres
|
|
8
|
+
* driver already exposes, which is what makes this adapter work with `pg`,
|
|
9
|
+
* `postgres.js`, Kysely, Neon, or anything else without an ORM in between.
|
|
10
|
+
*/
|
|
11
|
+
type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;
|
|
12
|
+
type PostgresAdapterOptions = {
|
|
13
|
+
/** Table-name prefix. Must match the instance's `tablePrefix`. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Runs `fn` inside a transaction, passing it a query function bound to that
|
|
17
|
+
* transaction. Supply it and `createNotifications` becomes atomic — a
|
|
18
|
+
* notification and its deliveries land together or neither does. Without it
|
|
19
|
+
* the adapter still works; it just loses that guarantee, the same tradeoff
|
|
20
|
+
* the Mongo adapter makes when no client is passed.
|
|
21
|
+
*/
|
|
22
|
+
transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Postgres, through any driver — no ORM required.
|
|
26
|
+
*
|
|
27
|
+
* `drizzleAdapter` predates this and is kept for people already on Drizzle,
|
|
28
|
+
* but it only ever used Drizzle as a SQL builder. This takes the query
|
|
29
|
+
* function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a
|
|
30
|
+
* serverless driver all work without pulling an ORM into the dependency tree.
|
|
31
|
+
*
|
|
32
|
+
* The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP
|
|
33
|
+
* LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim
|
|
34
|
+
* primitive depends on the second of those. See RFC 0003.
|
|
35
|
+
*/
|
|
36
|
+
declare function postgresAdapter(query: SqlQuery, options?: PostgresAdapterOptions): DatabaseAdapter;
|
|
37
|
+
|
|
38
|
+
export { type PostgresAdapterOptions, type SqlQuery, postgresAdapter };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-f0OR2DhY.js';
|
|
2
|
+
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Run one parameterised statement and return its rows.
|
|
6
|
+
*
|
|
7
|
+
* This is the whole contract — the lowest common denominator every Postgres
|
|
8
|
+
* driver already exposes, which is what makes this adapter work with `pg`,
|
|
9
|
+
* `postgres.js`, Kysely, Neon, or anything else without an ORM in between.
|
|
10
|
+
*/
|
|
11
|
+
type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;
|
|
12
|
+
type PostgresAdapterOptions = {
|
|
13
|
+
/** Table-name prefix. Must match the instance's `tablePrefix`. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Runs `fn` inside a transaction, passing it a query function bound to that
|
|
17
|
+
* transaction. Supply it and `createNotifications` becomes atomic — a
|
|
18
|
+
* notification and its deliveries land together or neither does. Without it
|
|
19
|
+
* the adapter still works; it just loses that guarantee, the same tradeoff
|
|
20
|
+
* the Mongo adapter makes when no client is passed.
|
|
21
|
+
*/
|
|
22
|
+
transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Postgres, through any driver — no ORM required.
|
|
26
|
+
*
|
|
27
|
+
* `drizzleAdapter` predates this and is kept for people already on Drizzle,
|
|
28
|
+
* but it only ever used Drizzle as a SQL builder. This takes the query
|
|
29
|
+
* function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a
|
|
30
|
+
* serverless driver all work without pulling an ORM into the dependency tree.
|
|
31
|
+
*
|
|
32
|
+
* The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP
|
|
33
|
+
* LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim
|
|
34
|
+
* primitive depends on the second of those. See RFC 0003.
|
|
35
|
+
*/
|
|
36
|
+
declare function postgresAdapter(query: SqlQuery, options?: PostgresAdapterOptions): DatabaseAdapter;
|
|
37
|
+
|
|
38
|
+
export { type PostgresAdapterOptions, type SqlQuery, postgresAdapter };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { isOperator } from '../chunk-OZSOIX23.js';
|
|
2
|
+
import { decodeBase64Url, encodeBase64Url } from '../chunk-PTGHMCCG.js';
|
|
3
|
+
import { toSnakeCase } from '../chunk-NYDVNMKG.js';
|
|
4
|
+
|
|
5
|
+
// src/adapters/postgres/statement.ts
|
|
6
|
+
var Statement = class {
|
|
7
|
+
#text = "";
|
|
8
|
+
params = [];
|
|
9
|
+
/** Raw SQL. Never pass user input through this — use `value`. */
|
|
10
|
+
raw(fragment) {
|
|
11
|
+
this.#text += fragment;
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
/** Binds a value and writes its placeholder. */
|
|
15
|
+
value(value) {
|
|
16
|
+
this.params.push(value instanceof Date ? value.toISOString() : value);
|
|
17
|
+
this.#text += `$${this.params.length}`;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
/** `$1, $2, $3` — a comma-separated run of bound values. */
|
|
21
|
+
list(values, separator = ", ") {
|
|
22
|
+
values.forEach((value, index) => {
|
|
23
|
+
if (index > 0) this.raw(separator);
|
|
24
|
+
this.value(value);
|
|
25
|
+
});
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/** `($1::text, $2::timestamptz)` — one VALUES tuple with explicit casts. */
|
|
29
|
+
tuple(cells) {
|
|
30
|
+
this.raw("(");
|
|
31
|
+
cells.forEach(([value, cast], index) => {
|
|
32
|
+
if (index > 0) this.raw(", ");
|
|
33
|
+
this.value(value).raw(`::${cast}`);
|
|
34
|
+
});
|
|
35
|
+
return this.raw(")");
|
|
36
|
+
}
|
|
37
|
+
get text() {
|
|
38
|
+
return this.#text;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var quote = (identifier) => `"${identifier.replace(/"/g, '""')}"`;
|
|
42
|
+
|
|
43
|
+
// src/adapters/postgres/adapter.ts
|
|
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 encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
|
|
50
|
+
function decodeCursor(cursor) {
|
|
51
|
+
const decoded = decodeBase64Url(cursor);
|
|
52
|
+
if (!decoded) return null;
|
|
53
|
+
const [iso, id] = decoded.split("|");
|
|
54
|
+
if (!iso || !id) return null;
|
|
55
|
+
const createdAt = new Date(iso);
|
|
56
|
+
return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
|
|
57
|
+
}
|
|
58
|
+
function toNotification(row) {
|
|
59
|
+
return {
|
|
60
|
+
id: str(row.id),
|
|
61
|
+
userId: str(row.user_id),
|
|
62
|
+
type: str(row.type),
|
|
63
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
64
|
+
actorId: nullableStr(row.actor_id),
|
|
65
|
+
groupKey: nullableStr(row.group_key),
|
|
66
|
+
dedupeKey: nullableStr(row.dedupe_key),
|
|
67
|
+
seenAt: nullableDate(row.seen_at),
|
|
68
|
+
readAt: nullableDate(row.read_at),
|
|
69
|
+
archivedAt: nullableDate(row.archived_at),
|
|
70
|
+
createdAt: date(row.created_at)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function toDelivery(row) {
|
|
74
|
+
return {
|
|
75
|
+
id: str(row.id),
|
|
76
|
+
notificationId: str(row.notification_id),
|
|
77
|
+
channel: str(row.channel),
|
|
78
|
+
status: str(row.status),
|
|
79
|
+
attempts: num(row.attempts),
|
|
80
|
+
maxAttempts: num(row.max_attempts),
|
|
81
|
+
notBefore: date(row.not_before),
|
|
82
|
+
claimedAt: nullableDate(row.claimed_at),
|
|
83
|
+
claimedBy: nullableStr(row.claimed_by),
|
|
84
|
+
lastError: nullableStr(row.last_error),
|
|
85
|
+
updatedAt: date(row.updated_at)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function appendWhere(statement, where) {
|
|
89
|
+
const entries = Object.entries(where);
|
|
90
|
+
if (entries.length === 0) return;
|
|
91
|
+
statement.raw(" WHERE ");
|
|
92
|
+
entries.forEach(([field, condition], index) => {
|
|
93
|
+
if (index > 0) statement.raw(" AND ");
|
|
94
|
+
const column = quote(toSnakeCase(field));
|
|
95
|
+
if (condition === null) {
|
|
96
|
+
statement.raw(`${column} IS NULL`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!isOperator(condition)) {
|
|
100
|
+
statement.raw(`${column} = `).value(condition);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const operator = condition;
|
|
104
|
+
if ("in" in operator) {
|
|
105
|
+
const list = operator.in;
|
|
106
|
+
if (list.length === 0) statement.raw("FALSE");
|
|
107
|
+
else statement.raw(`${column} IN (`).list(list).raw(")");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if ("lt" in operator) statement.raw(`${column} < `).value(operator.lt);
|
|
111
|
+
else if ("lte" in operator) statement.raw(`${column} <= `).value(operator.lte);
|
|
112
|
+
else if ("gt" in operator) statement.raw(`${column} > `).value(operator.gt);
|
|
113
|
+
else if ("gte" in operator) statement.raw(`${column} >= `).value(operator.gte);
|
|
114
|
+
else if ("not" in operator) {
|
|
115
|
+
if (operator.not === null) statement.raw(`${column} IS NOT NULL`);
|
|
116
|
+
else statement.raw(`${column} IS DISTINCT FROM `).value(operator.not);
|
|
117
|
+
} else statement.raw("TRUE");
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function postgresAdapter(query, options = {}) {
|
|
121
|
+
const prefix = options.prefix ?? "";
|
|
122
|
+
const NOTIFICATION = quote(`${prefix}notification`);
|
|
123
|
+
const DELIVERY = quote(`${prefix}notification_delivery`);
|
|
124
|
+
const run = async (statement, exec = query) => exec(statement.text, statement.params);
|
|
125
|
+
const atomically = (fn) => options.transaction ? options.transaction(fn) : fn(query);
|
|
126
|
+
return {
|
|
127
|
+
name: "postgres",
|
|
128
|
+
naming: "snake_case",
|
|
129
|
+
serializesJson: true,
|
|
130
|
+
async createNotifications(input) {
|
|
131
|
+
if (input.length === 0) return { created: [], deduped: [] };
|
|
132
|
+
const now = /* @__PURE__ */ new Date();
|
|
133
|
+
return atomically(async (exec) => {
|
|
134
|
+
const insert = new Statement().raw(
|
|
135
|
+
`INSERT INTO ${NOTIFICATION} (id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES `
|
|
136
|
+
);
|
|
137
|
+
input.forEach((row, index) => {
|
|
138
|
+
if (index > 0) insert.raw(", ");
|
|
139
|
+
insert.tuple([
|
|
140
|
+
[row.id, "text"],
|
|
141
|
+
[row.userId, "text"],
|
|
142
|
+
[row.type, "text"],
|
|
143
|
+
[JSON.stringify(row.payload ?? null), "jsonb"],
|
|
144
|
+
[row.actorId ?? null, "text"],
|
|
145
|
+
[row.groupKey ?? null, "text"],
|
|
146
|
+
[row.dedupeKey ?? null, "text"],
|
|
147
|
+
[now, "timestamptz"]
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
insert.raw(" ON CONFLICT (user_id, dedupe_key) DO NOTHING RETURNING id");
|
|
151
|
+
const inserted = await run(insert, exec);
|
|
152
|
+
const created = new Set(inserted.map((row) => str(row.id)));
|
|
153
|
+
const deliveries = input.filter((row) => created.has(row.id)).flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));
|
|
154
|
+
if (deliveries.length > 0) {
|
|
155
|
+
const insertDeliveries = new Statement().raw(
|
|
156
|
+
`INSERT INTO ${DELIVERY} (id, notification_id, channel, max_attempts, not_before, updated_at) VALUES `
|
|
157
|
+
);
|
|
158
|
+
deliveries.forEach(({ row, delivery }, index) => {
|
|
159
|
+
if (index > 0) insertDeliveries.raw(", ");
|
|
160
|
+
insertDeliveries.tuple([
|
|
161
|
+
[delivery.id, "text"],
|
|
162
|
+
[row.id, "text"],
|
|
163
|
+
[delivery.channel, "text"],
|
|
164
|
+
[delivery.maxAttempts, "integer"],
|
|
165
|
+
[delivery.notBefore, "timestamptz"],
|
|
166
|
+
[now, "timestamptz"]
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
await run(insertDeliveries, exec);
|
|
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 statement = new Statement().raw(`WITH claimed AS (UPDATE ${DELIVERY} SET status = 'claimed', claimed_at = `).value(now).raw("::timestamptz, claimed_by = ").value(args.claimToken).raw(`::text WHERE id IN (SELECT id FROM ${DELIVERY} WHERE (status = 'pending' OR `).raw("(status = 'claimed' AND claimed_at < ").value(staleBefore).raw("::timestamptz)) AND not_before <= ").value(now).raw("::timestamptz AND attempts < max_attempts");
|
|
181
|
+
if (args.channels && args.channels.length > 0) {
|
|
182
|
+
statement.raw(" AND channel IN (").list(args.channels).raw(")");
|
|
183
|
+
}
|
|
184
|
+
if (args.ids && args.ids.length > 0) {
|
|
185
|
+
statement.raw(" AND id IN (").list(args.ids).raw(")");
|
|
186
|
+
}
|
|
187
|
+
statement.raw(" ORDER BY not_before ASC, id ASC LIMIT ").value(args.limit).raw(" FOR UPDATE SKIP LOCKED)").raw(" RETURNING id, notification_id, channel, attempts, max_attempts)").raw(
|
|
188
|
+
` SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts, n.user_id, n.type, n.payload, n.actor_id FROM claimed c JOIN ${NOTIFICATION} n ON n.id = c.notification_id`
|
|
189
|
+
);
|
|
190
|
+
const rows = await run(statement);
|
|
191
|
+
return rows.map((row) => ({
|
|
192
|
+
id: str(row.id),
|
|
193
|
+
notificationId: str(row.notification_id),
|
|
194
|
+
channel: str(row.channel),
|
|
195
|
+
attempts: num(row.attempts),
|
|
196
|
+
maxAttempts: num(row.max_attempts),
|
|
197
|
+
notification: {
|
|
198
|
+
userId: str(row.user_id),
|
|
199
|
+
type: str(row.type),
|
|
200
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
201
|
+
actorId: nullableStr(row.actor_id)
|
|
202
|
+
}
|
|
203
|
+
}));
|
|
204
|
+
},
|
|
205
|
+
async releaseDeliveries(releases) {
|
|
206
|
+
if (releases.length === 0) return;
|
|
207
|
+
const now = /* @__PURE__ */ new Date();
|
|
208
|
+
const statement = new Statement().raw(
|
|
209
|
+
`UPDATE ${DELIVERY} d SET status = CASE WHEN v.result = 'sent' THEN 'sent' WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending' ELSE 'failed' END, attempts = CASE WHEN v.result = 'sent' THEN d.attempts ELSE d.attempts + 1 END, last_error = v.error, not_before = COALESCE(v.not_before, d.not_before), claimed_at = NULL, claimed_by = NULL, updated_at = `
|
|
210
|
+
);
|
|
211
|
+
statement.value(now).raw("::timestamptz FROM (VALUES ");
|
|
212
|
+
releases.forEach(({ id, outcome, nextAttemptAt }, index) => {
|
|
213
|
+
if (index > 0) statement.raw(", ");
|
|
214
|
+
statement.tuple([
|
|
215
|
+
[id, "text"],
|
|
216
|
+
[outcome.result, "text"],
|
|
217
|
+
[outcome.result === "failed" && outcome.retryable, "boolean"],
|
|
218
|
+
[outcome.result === "failed" ? outcome.error.slice(0, 2e3) : null, "text"],
|
|
219
|
+
[nextAttemptAt ?? null, "timestamptz"]
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
statement.raw(") AS v(id, result, retryable, error, not_before) WHERE d.id = v.id");
|
|
223
|
+
await run(statement);
|
|
224
|
+
},
|
|
225
|
+
async listNotifications(feed) {
|
|
226
|
+
const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;
|
|
227
|
+
const statement = new Statement().raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `).value(feed.userId).raw("::text AND archived_at IS NULL");
|
|
228
|
+
if (feed.unreadOnly) statement.raw(" AND read_at IS NULL");
|
|
229
|
+
if (cursor) {
|
|
230
|
+
statement.raw(" AND (created_at, id) < (").value(cursor.createdAt).raw("::timestamptz, ").value(cursor.id).raw("::text)");
|
|
231
|
+
}
|
|
232
|
+
statement.raw(" ORDER BY created_at DESC, id DESC LIMIT ").value(feed.limit + 1);
|
|
233
|
+
const rows = await run(statement);
|
|
234
|
+
const page = rows.slice(0, feed.limit).map(toNotification);
|
|
235
|
+
const last = page.at(-1);
|
|
236
|
+
return {
|
|
237
|
+
notifications: page,
|
|
238
|
+
nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
async countUnseen(userId) {
|
|
242
|
+
const rows = await run(
|
|
243
|
+
new Statement().raw(`SELECT count(*)::int AS count FROM ${NOTIFICATION} WHERE user_id = `).value(userId).raw("::text AND seen_at IS NULL AND archived_at IS NULL")
|
|
244
|
+
);
|
|
245
|
+
return num(rows[0]?.count ?? 0);
|
|
246
|
+
},
|
|
247
|
+
async markSeen(userId, before) {
|
|
248
|
+
await run(
|
|
249
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET seen_at = `).value(/* @__PURE__ */ new Date()).raw("::timestamptz WHERE user_id = ").value(userId).raw("::text AND seen_at IS NULL AND created_at <= ").value(before).raw("::timestamptz")
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
async markRead(userId, notificationIds) {
|
|
253
|
+
if (notificationIds.length === 0) return 0;
|
|
254
|
+
const rows = await run(
|
|
255
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET read_at = COALESCE(read_at, `).value(/* @__PURE__ */ new Date()).raw("::timestamptz) WHERE user_id = ").value(userId).raw("::text AND id IN (").list(notificationIds).raw(") RETURNING id")
|
|
256
|
+
);
|
|
257
|
+
return rows.length;
|
|
258
|
+
},
|
|
259
|
+
async markAllRead(userId) {
|
|
260
|
+
const rows = await run(
|
|
261
|
+
new Statement().raw(`UPDATE ${NOTIFICATION} SET read_at = `).value(/* @__PURE__ */ new Date()).raw("::timestamptz WHERE user_id = ").value(userId).raw("::text AND read_at IS NULL RETURNING id")
|
|
262
|
+
);
|
|
263
|
+
return rows.length;
|
|
264
|
+
},
|
|
265
|
+
async getFailedDeliveries(args) {
|
|
266
|
+
const rows = await run(
|
|
267
|
+
new Statement().raw(`SELECT * FROM ${DELIVERY} WHERE status = 'failed' AND updated_at >= `).value(args.since).raw("::timestamptz ORDER BY updated_at DESC LIMIT ").value(args.limit)
|
|
268
|
+
);
|
|
269
|
+
return rows.map(toDelivery);
|
|
270
|
+
},
|
|
271
|
+
async queryTable(table, where, queryOptions) {
|
|
272
|
+
const statement = new Statement().raw(`SELECT * FROM ${quote(table)}`);
|
|
273
|
+
appendWhere(statement, where);
|
|
274
|
+
if (queryOptions.orderBy) {
|
|
275
|
+
const direction = queryOptions.orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
276
|
+
statement.raw(` ORDER BY ${quote(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);
|
|
277
|
+
}
|
|
278
|
+
if (queryOptions.limit) statement.raw(" LIMIT ").value(queryOptions.limit);
|
|
279
|
+
return [...await run(statement)];
|
|
280
|
+
},
|
|
281
|
+
async insertRows(table, rows, onConflict) {
|
|
282
|
+
if (rows.length === 0) return 0;
|
|
283
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
284
|
+
if (columns.length === 0) return 0;
|
|
285
|
+
const statement = new Statement().raw(
|
|
286
|
+
`INSERT INTO ${quote(table)} (${columns.map((c) => quote(toSnakeCase(c))).join(", ")}) VALUES `
|
|
287
|
+
);
|
|
288
|
+
rows.forEach((row, index) => {
|
|
289
|
+
if (index > 0) statement.raw(", ");
|
|
290
|
+
statement.raw("(").list(columns.map((column) => row[column])).raw(")");
|
|
291
|
+
});
|
|
292
|
+
if (onConflict && onConflict.length > 0) {
|
|
293
|
+
const assignments = columns.filter((column) => !onConflict.includes(column)).map((column) => {
|
|
294
|
+
const quoted = quote(toSnakeCase(column));
|
|
295
|
+
return `${quoted} = EXCLUDED.${quoted}`;
|
|
296
|
+
});
|
|
297
|
+
statement.raw(
|
|
298
|
+
` ON CONFLICT (${onConflict.map((f) => quote(toSnakeCase(f))).join(", ")}) DO UPDATE SET ${assignments.join(", ")}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
302
|
+
return (await run(statement)).length;
|
|
303
|
+
},
|
|
304
|
+
async updateRows(table, where, set) {
|
|
305
|
+
const assignments = Object.entries(set);
|
|
306
|
+
if (assignments.length === 0) return 0;
|
|
307
|
+
const statement = new Statement().raw(`UPDATE ${quote(table)} SET `);
|
|
308
|
+
assignments.forEach(([field, value], index) => {
|
|
309
|
+
if (index > 0) statement.raw(", ");
|
|
310
|
+
statement.raw(`${quote(toSnakeCase(field))} = `).value(value);
|
|
311
|
+
});
|
|
312
|
+
appendWhere(statement, where);
|
|
313
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
314
|
+
return (await run(statement)).length;
|
|
315
|
+
},
|
|
316
|
+
async deleteRows(table, where) {
|
|
317
|
+
const statement = new Statement().raw(`DELETE FROM ${quote(table)}`);
|
|
318
|
+
appendWhere(statement, where);
|
|
319
|
+
statement.raw(" RETURNING 1 AS ok");
|
|
320
|
+
return (await run(statement)).length;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export { postgresAdapter };
|
|
326
|
+
//# sourceMappingURL=postgres.js.map
|
|
327
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/postgres/statement.ts","../../src/adapters/postgres/adapter.ts"],"names":[],"mappings":"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","file":"postgres.js","sourcesContent":["/**\n * Builds a parameterised statement: SQL text with `$1, $2 …` placeholders plus\n * the values to bind, which is the one calling convention every Postgres\n * driver agrees on.\n *\n * Values are never interpolated. Identifiers are quoted, and the only\n * identifiers that reach here are either constants in this file or names the\n * PluginStore has already validated against the plugin's own schema.\n */\nexport class Statement {\n #text = \"\";\n readonly params: unknown[] = [];\n\n /** Raw SQL. Never pass user input through this — use `value`. */\n raw(fragment: string): this {\n this.#text += fragment;\n return this;\n }\n\n /** Binds a value and writes its placeholder. */\n value(value: unknown): this {\n // Dates bind as ISO strings: every driver accepts that for timestamptz,\n // whereas a Date object is only handled by some of them.\n this.params.push(value instanceof Date ? value.toISOString() : value);\n this.#text += `$${this.params.length}`;\n return this;\n }\n\n /** `$1, $2, $3` — a comma-separated run of bound values. */\n list(values: readonly unknown[], separator = \", \"): this {\n values.forEach((value, index) => {\n if (index > 0) this.raw(separator);\n this.value(value);\n });\n return this;\n }\n\n /** `($1::text, $2::timestamptz)` — one VALUES tuple with explicit casts. */\n tuple(cells: readonly (readonly [unknown, string])[]): this {\n this.raw(\"(\");\n cells.forEach(([value, cast], index) => {\n if (index > 0) this.raw(\", \");\n this.value(value).raw(`::${cast}`);\n });\n return this.raw(\")\");\n }\n\n get text(): string {\n return this.#text;\n }\n}\n\n/** Double-quoted so a table or column name can never be read as SQL. */\nexport const quote = (identifier: string): string => `\"${identifier.replace(/\"/g, '\"\"')}\"`;\n","import type {\n ClaimArgs,\n ClaimedDelivery,\n DatabaseAdapter,\n DeliveryRelease,\n FeedPage,\n FeedQuery,\n InsertNotification,\n} from \"../../core/adapter\";\nimport { decodeBase64Url, encodeBase64Url } from \"../../core/base64url\";\nimport { isOperator, type QueryOptions, type WhereClause } from \"../../core/store\";\nimport type { Channel, DeliveryRecord, DeliveryStatus, NotificationRecord } from \"../../core/types\";\nimport { toSnakeCase } from \"../../schema/declaration\";\nimport { quote, Statement } from \"./statement\";\n\ntype Row = Record<string, unknown>;\n\n/**\n * Run one parameterised statement and return its rows.\n *\n * This is the whole contract — the lowest common denominator every Postgres\n * driver already exposes, which is what makes this adapter work with `pg`,\n * `postgres.js`, Kysely, Neon, or anything else without an ORM in between.\n */\nexport type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;\n\nexport type PostgresAdapterOptions = {\n /** Table-name prefix. Must match the instance's `tablePrefix`. */\n prefix?: string;\n /**\n * Runs `fn` inside a transaction, passing it a query function bound to that\n * transaction. Supply it and `createNotifications` becomes atomic — a\n * notification and its deliveries land together or neither does. Without it\n * the adapter still works; it just loses that guarantee, the same tradeoff\n * the Mongo adapter makes when no client is passed.\n */\n transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;\n};\n\nconst str = (value: unknown): string => String(value);\nconst nullableStr = (value: unknown): string | null => (value == null ? null : String(value));\nconst num = (value: unknown): number => Number(value);\nconst date = (value: unknown): Date => (value instanceof Date ? value : new Date(String(value)));\nconst nullableDate = (value: unknown): Date | null =>\n value == null ? null : value instanceof Date ? value : new Date(String(value));\n\nconst encodeCursor = (createdAt: Date, id: string) =>\n encodeBase64Url(`${createdAt.toISOString()}|${id}`);\n\nfunction decodeCursor(cursor: string): { createdAt: Date; id: string } | null {\n const decoded = decodeBase64Url(cursor);\n if (!decoded) return null;\n\n const [iso, id] = decoded.split(\"|\");\n if (!iso || !id) return null;\n\n const createdAt = new Date(iso);\n return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };\n}\n\nfunction toNotification(row: Row): NotificationRecord {\n return {\n id: str(row.id),\n userId: str(row.user_id),\n type: str(row.type),\n payload: typeof row.payload === \"string\" ? JSON.parse(row.payload) : row.payload,\n actorId: nullableStr(row.actor_id),\n groupKey: nullableStr(row.group_key),\n dedupeKey: nullableStr(row.dedupe_key),\n seenAt: nullableDate(row.seen_at),\n readAt: nullableDate(row.read_at),\n archivedAt: nullableDate(row.archived_at),\n createdAt: date(row.created_at),\n };\n}\n\nfunction toDelivery(row: Row): DeliveryRecord {\n return {\n id: str(row.id),\n notificationId: str(row.notification_id),\n channel: str(row.channel) as Channel,\n status: str(row.status) as DeliveryStatus,\n attempts: num(row.attempts),\n maxAttempts: num(row.max_attempts),\n notBefore: date(row.not_before),\n claimedAt: nullableDate(row.claimed_at),\n claimedBy: nullableStr(row.claimed_by),\n lastError: nullableStr(row.last_error),\n updatedAt: date(row.updated_at),\n };\n}\n\n/** Appends ` WHERE …` for a PluginStore clause. Values are always bound. */\nfunction appendWhere(statement: Statement, where: WhereClause): void {\n const entries = Object.entries(where);\n if (entries.length === 0) return;\n\n statement.raw(\" WHERE \");\n\n entries.forEach(([field, condition], index) => {\n if (index > 0) statement.raw(\" AND \");\n const column = quote(toSnakeCase(field));\n\n if (condition === null) {\n statement.raw(`${column} IS NULL`);\n return;\n }\n\n if (!isOperator(condition)) {\n statement.raw(`${column} = `).value(condition);\n return;\n }\n\n const operator = condition as Record<string, unknown>;\n\n if (\"in\" in operator) {\n const list = operator.in as readonly (string | number)[];\n // An empty IN () is a syntax error, and matching nothing is the honest\n // reading of \"in this empty set\".\n if (list.length === 0) statement.raw(\"FALSE\");\n else statement.raw(`${column} IN (`).list(list).raw(\")\");\n return;\n }\n\n if (\"lt\" in operator) statement.raw(`${column} < `).value(operator.lt);\n else if (\"lte\" in operator) statement.raw(`${column} <= `).value(operator.lte);\n else if (\"gt\" in operator) statement.raw(`${column} > `).value(operator.gt);\n else if (\"gte\" in operator) statement.raw(`${column} >= `).value(operator.gte);\n else if (\"not\" in operator) {\n if (operator.not === null) statement.raw(`${column} IS NOT NULL`);\n else statement.raw(`${column} IS DISTINCT FROM `).value(operator.not);\n } else statement.raw(\"TRUE\");\n });\n}\n\n/**\n * Postgres, through any driver — no ORM required.\n *\n * `drizzleAdapter` predates this and is kept for people already on Drizzle,\n * but it only ever used Drizzle as a SQL builder. This takes the query\n * function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a\n * serverless driver all work without pulling an ORM into the dependency tree.\n *\n * The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP\n * LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim\n * primitive depends on the second of those. See RFC 0003.\n */\nexport function postgresAdapter(\n query: SqlQuery,\n options: PostgresAdapterOptions = {},\n): DatabaseAdapter {\n const prefix = options.prefix ?? \"\";\n\n const NOTIFICATION = quote(`${prefix}notification`);\n const DELIVERY = quote(`${prefix}notification_delivery`);\n\n const run = async (statement: Statement, exec: SqlQuery = query) =>\n exec(statement.text, statement.params);\n\n /** Uses a transaction when one was supplied, and runs plainly otherwise. */\n const atomically = <T>(fn: (exec: SqlQuery) => Promise<T>): Promise<T> =>\n options.transaction ? options.transaction(fn) : fn(query);\n\n return {\n name: \"postgres\",\n naming: \"snake_case\",\n serializesJson: true,\n\n async createNotifications(input: readonly InsertNotification[]) {\n if (input.length === 0) return { created: [], deduped: [] };\n\n // One clock. created_at has a DEFAULT now() for hand-written SQL, but\n // now() is the *database* clock while every cutoff compared against it\n // (markSeen, getFailedDeliveries) comes from the app. In production those\n // are different hosts, and NTP skew put the newest rows outside markSeen.\n const now = new Date();\n\n return atomically(async (exec) => {\n const insert = new Statement().raw(\n `INSERT INTO ${NOTIFICATION} ` +\n \"(id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES \",\n );\n\n input.forEach((row, index) => {\n if (index > 0) insert.raw(\", \");\n insert.tuple([\n [row.id, \"text\"],\n [row.userId, \"text\"],\n [row.type, \"text\"],\n [JSON.stringify(row.payload ?? null), \"jsonb\"],\n [row.actorId ?? null, \"text\"],\n [row.groupKey ?? null, \"text\"],\n [row.dedupeKey ?? null, \"text\"],\n [now, \"timestamptz\"],\n ]);\n });\n\n // Rows with a NULL dedupe_key never conflict — Postgres treats NULLs as\n // distinct — so unlimited undeduped notifications coexist.\n insert.raw(\" ON CONFLICT (user_id, dedupe_key) DO NOTHING RETURNING id\");\n\n const inserted = await run(insert, exec);\n const created = new Set(inserted.map((row) => str(row.id)));\n\n const deliveries = input\n .filter((row) => created.has(row.id))\n .flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));\n\n if (deliveries.length > 0) {\n const insertDeliveries = new Statement().raw(\n `INSERT INTO ${DELIVERY} ` +\n \"(id, notification_id, channel, max_attempts, not_before, updated_at) VALUES \",\n );\n\n deliveries.forEach(({ row, delivery }, index) => {\n if (index > 0) insertDeliveries.raw(\", \");\n insertDeliveries.tuple([\n [delivery.id, \"text\"],\n [row.id, \"text\"],\n [delivery.channel, \"text\"],\n [delivery.maxAttempts, \"integer\"],\n [delivery.notBefore, \"timestamptz\"],\n [now, \"timestamptz\"],\n ]);\n });\n\n await run(insertDeliveries, exec);\n }\n\n return {\n created: [...created],\n deduped: input.filter((row) => !created.has(row.id)).map((row) => row.id),\n };\n });\n },\n\n async claimPendingDeliveries(args: ClaimArgs): Promise<readonly ClaimedDelivery[]> {\n const now = args.now ?? new Date();\n const staleBefore = new Date(now.getTime() - args.leaseMs);\n\n // SKIP LOCKED is what lets concurrent sweeps step around each other\n // rather than block. The CTE joins the notification in the same round\n // trip — claiming 20 rows then looking each one up is the N+1 this\n // primitive exists to avoid. See RFC 0003 §5.\n const statement = new Statement()\n .raw(`WITH claimed AS (UPDATE ${DELIVERY} SET status = 'claimed', claimed_at = `)\n .value(now)\n .raw(\"::timestamptz, claimed_by = \")\n .value(args.claimToken)\n .raw(`::text WHERE id IN (SELECT id FROM ${DELIVERY} WHERE (status = 'pending' OR `)\n .raw(\"(status = 'claimed' AND claimed_at < \")\n .value(staleBefore)\n .raw(\"::timestamptz)) AND not_before <= \")\n .value(now)\n .raw(\"::timestamptz AND attempts < max_attempts\");\n\n if (args.channels && args.channels.length > 0) {\n statement.raw(\" AND channel IN (\").list(args.channels).raw(\")\");\n }\n if (args.ids && args.ids.length > 0) {\n statement.raw(\" AND id IN (\").list(args.ids).raw(\")\");\n }\n\n statement\n .raw(\" ORDER BY not_before ASC, id ASC LIMIT \")\n .value(args.limit)\n .raw(\" FOR UPDATE SKIP LOCKED)\")\n .raw(\" RETURNING id, notification_id, channel, attempts, max_attempts)\")\n .raw(\n \" SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts,\" +\n \" n.user_id, n.type, n.payload, n.actor_id FROM claimed c\" +\n ` JOIN ${NOTIFICATION} n ON n.id = c.notification_id`,\n );\n\n const rows = await run(statement);\n\n return rows.map((row) => ({\n id: str(row.id),\n notificationId: str(row.notification_id),\n channel: str(row.channel) as Channel,\n attempts: num(row.attempts),\n maxAttempts: num(row.max_attempts),\n notification: {\n userId: str(row.user_id),\n type: str(row.type),\n payload: typeof row.payload === \"string\" ? JSON.parse(row.payload) : row.payload,\n actorId: nullableStr(row.actor_id),\n },\n }));\n },\n\n async releaseDeliveries(releases: readonly DeliveryRelease[]) {\n if (releases.length === 0) return;\n const now = new Date();\n\n // Written unconditionally — no claimed_by predicate. If the lease expired\n // and another worker re-sent, the duplicate already happened; recording\n // the true terminal state beats wedging the row in 'claimed'. RFC 0003 §6.\n const statement = new Statement().raw(\n `UPDATE ${DELIVERY} d SET status = CASE` +\n \" WHEN v.result = 'sent' THEN 'sent'\" +\n \" WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending'\" +\n \" ELSE 'failed' END,\" +\n \" attempts = CASE WHEN v.result = 'sent' THEN d.attempts ELSE d.attempts + 1 END,\" +\n \" last_error = v.error,\" +\n \" not_before = COALESCE(v.not_before, d.not_before),\" +\n \" claimed_at = NULL, claimed_by = NULL, updated_at = \",\n );\n\n statement.value(now).raw(\"::timestamptz FROM (VALUES \");\n\n releases.forEach(({ id, outcome, nextAttemptAt }, index) => {\n if (index > 0) statement.raw(\", \");\n statement.tuple([\n [id, \"text\"],\n [outcome.result, \"text\"],\n [outcome.result === \"failed\" && outcome.retryable, \"boolean\"],\n [outcome.result === \"failed\" ? outcome.error.slice(0, 2000) : null, \"text\"],\n [nextAttemptAt ?? null, \"timestamptz\"],\n ]);\n });\n\n statement.raw(\") AS v(id, result, retryable, error, not_before) WHERE d.id = v.id\");\n\n await run(statement);\n },\n\n async listNotifications(feed: FeedQuery): Promise<FeedPage> {\n const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;\n\n const statement = new Statement()\n .raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `)\n .value(feed.userId)\n .raw(\"::text AND archived_at IS NULL\");\n\n if (feed.unreadOnly) statement.raw(\" AND read_at IS NULL\");\n\n if (cursor) {\n statement\n .raw(\" AND (created_at, id) < (\")\n .value(cursor.createdAt)\n .raw(\"::timestamptz, \")\n .value(cursor.id)\n .raw(\"::text)\");\n }\n\n statement.raw(\" ORDER BY created_at DESC, id DESC LIMIT \").value(feed.limit + 1);\n\n const rows = await run(statement);\n const page = rows.slice(0, feed.limit).map(toNotification);\n const last = page.at(-1);\n\n return {\n notifications: page,\n nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null,\n };\n },\n\n async countUnseen(userId: string) {\n const rows = await run(\n new Statement()\n .raw(`SELECT count(*)::int AS count FROM ${NOTIFICATION} WHERE user_id = `)\n .value(userId)\n .raw(\"::text AND seen_at IS NULL AND archived_at IS NULL\"),\n );\n return num(rows[0]?.count ?? 0);\n },\n\n async markSeen(userId: string, before: Date) {\n await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET seen_at = `)\n .value(new Date())\n .raw(\"::timestamptz WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND seen_at IS NULL AND created_at <= \")\n .value(before)\n .raw(\"::timestamptz\"),\n );\n },\n\n async markRead(userId: string, notificationIds: readonly string[]) {\n if (notificationIds.length === 0) return 0;\n\n // Scoped by user_id as well as id — a caller must never be able to flip\n // someone else's row by guessing an id. RFC 0002 §2.\n // No `read_at IS NULL` filter: re-marking must be idempotent. Returning 0\n // for an already-read row made the route 404, which made the client roll\n // its optimistic update back and show the item as unread again.\n const rows = await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET read_at = COALESCE(read_at, `)\n .value(new Date())\n .raw(\"::timestamptz) WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND id IN (\")\n .list(notificationIds)\n .raw(\") RETURNING id\"),\n );\n return rows.length;\n },\n\n async markAllRead(userId: string) {\n const rows = await run(\n new Statement()\n .raw(`UPDATE ${NOTIFICATION} SET read_at = `)\n .value(new Date())\n .raw(\"::timestamptz WHERE user_id = \")\n .value(userId)\n .raw(\"::text AND read_at IS NULL RETURNING id\"),\n );\n return rows.length;\n },\n\n async getFailedDeliveries(args: { since: Date; limit: number }) {\n const rows = await run(\n new Statement()\n .raw(`SELECT * FROM ${DELIVERY} WHERE status = 'failed' AND updated_at >= `)\n .value(args.since)\n .raw(\"::timestamptz ORDER BY updated_at DESC LIMIT \")\n .value(args.limit),\n );\n return rows.map(toDelivery);\n },\n\n async queryTable(table: string, where: WhereClause, queryOptions: QueryOptions) {\n const statement = new Statement().raw(`SELECT * FROM ${quote(table)}`);\n appendWhere(statement, where);\n\n if (queryOptions.orderBy) {\n const direction = queryOptions.orderBy.direction === \"desc\" ? \"DESC\" : \"ASC\";\n statement.raw(` ORDER BY ${quote(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);\n }\n if (queryOptions.limit) statement.raw(\" LIMIT \").value(queryOptions.limit);\n\n return [...(await run(statement))];\n },\n\n async insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ) {\n if (rows.length === 0) return 0;\n\n // Column order is taken from the first row and every row is projected\n // onto it, so a ragged batch cannot shift values into other columns.\n const columns = Object.keys(rows[0] ?? {});\n if (columns.length === 0) return 0;\n\n const statement = new Statement().raw(\n `INSERT INTO ${quote(table)} (${columns.map((c) => quote(toSnakeCase(c))).join(\", \")}) VALUES `,\n );\n\n rows.forEach((row, index) => {\n if (index > 0) statement.raw(\", \");\n statement\n .raw(\"(\")\n .list(columns.map((column) => row[column]))\n .raw(\")\");\n });\n\n if (onConflict && onConflict.length > 0) {\n const assignments = columns\n .filter((column) => !onConflict.includes(column))\n .map((column) => {\n const quoted = quote(toSnakeCase(column));\n return `${quoted} = EXCLUDED.${quoted}`;\n });\n\n statement.raw(\n ` ON CONFLICT (${onConflict.map((f) => quote(toSnakeCase(f))).join(\", \")}) DO UPDATE SET ${assignments.join(\", \")}`,\n );\n }\n\n statement.raw(\" RETURNING 1 AS ok\");\n return (await run(statement)).length;\n },\n\n async updateRows(table: string, where: WhereClause, set: Record<string, unknown>) {\n const assignments = Object.entries(set);\n if (assignments.length === 0) return 0;\n\n const statement = new Statement().raw(`UPDATE ${quote(table)} SET `);\n\n assignments.forEach(([field, value], index) => {\n if (index > 0) statement.raw(\", \");\n statement.raw(`${quote(toSnakeCase(field))} = `).value(value);\n });\n\n appendWhere(statement, where);\n statement.raw(\" RETURNING 1 AS ok\");\n\n return (await run(statement)).length;\n },\n\n async deleteRows(table: string, where: WhereClause) {\n const statement = new Statement().raw(`DELETE FROM ${quote(table)}`);\n appendWhere(statement, where);\n statement.raw(\" RETURNING 1 AS ok\");\n\n return (await run(statement)).length;\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-ping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Self-hosted, type-safe notifications for TypeScript apps. Your database, your users.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Teklu Moges",
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"import": "./dist/adapters/mongodb.js",
|
|
60
60
|
"require": "./dist/adapters/mongodb.cjs"
|
|
61
61
|
},
|
|
62
|
+
"./adapters/postgres": {
|
|
63
|
+
"types": "./dist/adapters/postgres.d.ts",
|
|
64
|
+
"import": "./dist/adapters/postgres.js",
|
|
65
|
+
"require": "./dist/adapters/postgres.cjs"
|
|
66
|
+
},
|
|
62
67
|
"./schema": {
|
|
63
68
|
"types": "./dist/schema.d.ts",
|
|
64
69
|
"import": "./dist/schema.js",
|
|
@@ -138,15 +143,6 @@
|
|
|
138
143
|
"optional": true
|
|
139
144
|
}
|
|
140
145
|
},
|
|
141
|
-
"scripts": {
|
|
142
|
-
"build": "node scripts/clean.mjs && tsup",
|
|
143
|
-
"dev": "tsup --watch",
|
|
144
|
-
"test": "vitest run",
|
|
145
|
-
"typecheck": "tsc --noEmit",
|
|
146
|
-
"type-budget": "node scripts/check-type-budget.mjs",
|
|
147
|
-
"check-node-floor": "node scripts/check-node-floor.mjs",
|
|
148
|
-
"prepack": "node scripts/copy-docs.mjs"
|
|
149
|
-
},
|
|
150
146
|
"devDependencies": {
|
|
151
147
|
"@types/node": "^26.1.2",
|
|
152
148
|
"@types/react": "^19.2.18",
|
|
@@ -164,5 +160,13 @@
|
|
|
164
160
|
},
|
|
165
161
|
"dependencies": {
|
|
166
162
|
"@standard-schema/spec": "^1.1.0"
|
|
163
|
+
},
|
|
164
|
+
"scripts": {
|
|
165
|
+
"build": "node scripts/clean.mjs && tsup",
|
|
166
|
+
"dev": "tsup --watch",
|
|
167
|
+
"test": "vitest run",
|
|
168
|
+
"typecheck": "tsc --noEmit",
|
|
169
|
+
"type-budget": "node scripts/check-type-budget.mjs",
|
|
170
|
+
"check-node-floor": "node scripts/check-node-floor.mjs"
|
|
167
171
|
}
|
|
168
|
-
}
|
|
172
|
+
}
|