easy-ping 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +44 -2
  2. package/dist/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
  3. package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
  4. package/dist/adapters/drizzle.cjs +6 -2
  5. package/dist/adapters/drizzle.cjs.map +1 -1
  6. package/dist/adapters/drizzle.d.cts +1 -1
  7. package/dist/adapters/drizzle.d.ts +1 -1
  8. package/dist/adapters/drizzle.js +6 -2
  9. package/dist/adapters/drizzle.js.map +1 -1
  10. package/dist/adapters/mongodb.cjs +3 -2
  11. package/dist/adapters/mongodb.cjs.map +1 -1
  12. package/dist/adapters/mongodb.d.cts +2 -2
  13. package/dist/adapters/mongodb.d.ts +2 -2
  14. package/dist/adapters/mongodb.js +3 -2
  15. package/dist/adapters/mongodb.js.map +1 -1
  16. package/dist/adapters/postgres.cjs +484 -0
  17. package/dist/adapters/postgres.cjs.map +1 -0
  18. package/dist/adapters/postgres.d.cts +81 -0
  19. package/dist/adapters/postgres.d.ts +81 -0
  20. package/dist/adapters/postgres.js +360 -0
  21. package/dist/adapters/postgres.js.map +1 -0
  22. package/dist/chunk-RBDW4S6F.js +199 -0
  23. package/dist/chunk-RBDW4S6F.js.map +1 -0
  24. package/dist/client.cjs +3 -5
  25. package/dist/client.cjs.map +1 -1
  26. package/dist/client.js +3 -5
  27. package/dist/client.js.map +1 -1
  28. package/dist/index.cjs +17 -11
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +6 -4
  31. package/dist/index.d.ts +6 -4
  32. package/dist/index.js +17 -11
  33. package/dist/index.js.map +1 -1
  34. package/dist/{plugin-bTJKmnlq.d.ts → plugin-CmPOFVmn.d.ts} +1 -1
  35. package/dist/{plugin-Bqj9xAOS.d.cts → plugin-f5KaFWSN.d.cts} +1 -1
  36. package/dist/plugins/digests.d.cts +2 -2
  37. package/dist/plugins/digests.d.ts +2 -2
  38. package/dist/plugins/preferences-client.d.cts +2 -2
  39. package/dist/plugins/preferences-client.d.ts +2 -2
  40. package/dist/plugins/preferences.d.cts +2 -2
  41. package/dist/plugins/preferences.d.ts +2 -2
  42. package/dist/plugins/push.cjs +25 -23
  43. package/dist/plugins/push.cjs.map +1 -1
  44. package/dist/plugins/push.d.cts +59 -3
  45. package/dist/plugins/push.d.ts +59 -3
  46. package/dist/plugins/push.js +25 -24
  47. package/dist/plugins/push.js.map +1 -1
  48. package/dist/providers/web-push.d.cts +2 -2
  49. package/dist/providers/web-push.d.ts +2 -2
  50. package/dist/react.cjs +3 -5
  51. package/dist/react.cjs.map +1 -1
  52. package/dist/react.js +3 -5
  53. package/dist/react.js.map +1 -1
  54. package/dist/schema.d.cts +2 -2
  55. package/dist/schema.d.ts +2 -2
  56. package/dist/schema.js +1 -197
  57. package/dist/schema.js.map +1 -1
  58. package/dist/testing.cjs +29 -0
  59. package/dist/testing.cjs.map +1 -1
  60. package/dist/testing.d.cts +1 -1
  61. package/dist/testing.d.ts +1 -1
  62. package/dist/testing.js +29 -0
  63. package/dist/testing.js.map +1 -1
  64. package/package.json +15 -11
@@ -0,0 +1,81 @@
1
+ import { D as DatabaseAdapter } from '../adapter-BfLe2J0t.cjs';
2
+ import { S as SchemaDeclaration } from '../plugin-f5KaFWSN.cjs';
3
+
4
+ type Row = Record<string, unknown>;
5
+ /**
6
+ * Run one parameterised statement and return its rows.
7
+ *
8
+ * This is the whole contract — the lowest common denominator every Postgres
9
+ * driver already exposes, which is what makes this adapter work with `pg`,
10
+ * `postgres.js`, Kysely, Neon, or anything else without an ORM in between.
11
+ */
12
+ type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;
13
+ type PostgresAdapterOptions = {
14
+ /** Table-name prefix. Must match the instance's `tablePrefix`. */
15
+ prefix?: string;
16
+ /**
17
+ * Runs `fn` inside a transaction, passing it a query function bound to that
18
+ * transaction. Supply it and `createNotifications` becomes atomic — a
19
+ * notification and its deliveries land together or neither does. Without it
20
+ * the adapter still works; it just loses that guarantee, the same tradeoff
21
+ * the Mongo adapter makes when no client is passed.
22
+ */
23
+ transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
24
+ };
25
+ /**
26
+ * Postgres, through any driver — no ORM required.
27
+ *
28
+ * `drizzleAdapter` predates this and is kept for people already on Drizzle,
29
+ * but it only ever used Drizzle as a SQL builder. This takes the query
30
+ * function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a
31
+ * serverless driver all work without pulling an ORM into the dependency tree.
32
+ *
33
+ * The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP
34
+ * LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim
35
+ * primitive depends on the second of those. See RFC 0003.
36
+ */
37
+ declare function postgresAdapter(query: SqlQuery, options?: PostgresAdapterOptions): DatabaseAdapter;
38
+
39
+ /**
40
+ * The slice of `pg`'s Pool this needs, declared structurally so the library
41
+ * takes no dependency on the driver. `postgres.js` and Kysely have their own
42
+ * transaction helpers and do not need this one.
43
+ */
44
+ type PgPoolLike = {
45
+ connect(): Promise<{
46
+ query(text: string, params?: readonly unknown[]): Promise<{
47
+ rows: Record<string, unknown>[];
48
+ }>;
49
+ release(): void;
50
+ }>;
51
+ };
52
+ /**
53
+ * The `transaction` option for `node-postgres`, so callers stop hand-writing
54
+ * the same sixteen lines of BEGIN/COMMIT/ROLLBACK/release.
55
+ *
56
+ * postgresAdapter(query, { transaction: pgTransaction(pool) })
57
+ *
58
+ * The `finally` is the part worth having in one place: drop the release and
59
+ * every send leaks a connection until the pool is exhausted.
60
+ */
61
+ declare function pgTransaction(pool: PgPoolLike): <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
62
+ type CreateTablesOptions = {
63
+ /** Must match the prefix given to the adapter. */
64
+ prefix?: string;
65
+ /**
66
+ * Schemas owned by plugins, exported alongside each plugin — `pushSchema`
67
+ * from `easy-ping/plugins/push`, for instance.
68
+ */
69
+ plugins?: readonly SchemaDeclaration[];
70
+ };
71
+ /**
72
+ * Creates the core tables, plus any plugin tables passed, and returns how many
73
+ * statements ran.
74
+ *
75
+ * Every statement is `IF NOT EXISTS`, so this is safe to run on every boot.
76
+ * It is a bootstrap, not a migration: it never alters a table that already
77
+ * exists. Once a database is live, use `planPostgresMigration` instead.
78
+ */
79
+ declare function createPostgresTables(query: SqlQuery, options?: CreateTablesOptions): Promise<number>;
80
+
81
+ export { type CreateTablesOptions, type PgPoolLike, type PostgresAdapterOptions, type SqlQuery, createPostgresTables, pgTransaction, postgresAdapter };
@@ -0,0 +1,81 @@
1
+ import { D as DatabaseAdapter } from '../adapter-BfLe2J0t.js';
2
+ import { S as SchemaDeclaration } from '../plugin-CmPOFVmn.js';
3
+
4
+ type Row = Record<string, unknown>;
5
+ /**
6
+ * Run one parameterised statement and return its rows.
7
+ *
8
+ * This is the whole contract — the lowest common denominator every Postgres
9
+ * driver already exposes, which is what makes this adapter work with `pg`,
10
+ * `postgres.js`, Kysely, Neon, or anything else without an ORM in between.
11
+ */
12
+ type SqlQuery = (text: string, params: readonly unknown[]) => Promise<readonly Row[]>;
13
+ type PostgresAdapterOptions = {
14
+ /** Table-name prefix. Must match the instance's `tablePrefix`. */
15
+ prefix?: string;
16
+ /**
17
+ * Runs `fn` inside a transaction, passing it a query function bound to that
18
+ * transaction. Supply it and `createNotifications` becomes atomic — a
19
+ * notification and its deliveries land together or neither does. Without it
20
+ * the adapter still works; it just loses that guarantee, the same tradeoff
21
+ * the Mongo adapter makes when no client is passed.
22
+ */
23
+ transaction?: <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
24
+ };
25
+ /**
26
+ * Postgres, through any driver — no ORM required.
27
+ *
28
+ * `drizzleAdapter` predates this and is kept for people already on Drizzle,
29
+ * but it only ever used Drizzle as a SQL builder. This takes the query
30
+ * function directly instead, so a plain `pg` Pool, `postgres.js`, Kysely or a
31
+ * serverless driver all work without pulling an ORM into the dependency tree.
32
+ *
33
+ * The SQL is deliberately Postgres-specific — `ON CONFLICT`, `FOR UPDATE SKIP
34
+ * LOCKED` and `IS DISTINCT FROM` have no portable equivalent, and the claim
35
+ * primitive depends on the second of those. See RFC 0003.
36
+ */
37
+ declare function postgresAdapter(query: SqlQuery, options?: PostgresAdapterOptions): DatabaseAdapter;
38
+
39
+ /**
40
+ * The slice of `pg`'s Pool this needs, declared structurally so the library
41
+ * takes no dependency on the driver. `postgres.js` and Kysely have their own
42
+ * transaction helpers and do not need this one.
43
+ */
44
+ type PgPoolLike = {
45
+ connect(): Promise<{
46
+ query(text: string, params?: readonly unknown[]): Promise<{
47
+ rows: Record<string, unknown>[];
48
+ }>;
49
+ release(): void;
50
+ }>;
51
+ };
52
+ /**
53
+ * The `transaction` option for `node-postgres`, so callers stop hand-writing
54
+ * the same sixteen lines of BEGIN/COMMIT/ROLLBACK/release.
55
+ *
56
+ * postgresAdapter(query, { transaction: pgTransaction(pool) })
57
+ *
58
+ * The `finally` is the part worth having in one place: drop the release and
59
+ * every send leaks a connection until the pool is exhausted.
60
+ */
61
+ declare function pgTransaction(pool: PgPoolLike): <T>(fn: (query: SqlQuery) => Promise<T>) => Promise<T>;
62
+ type CreateTablesOptions = {
63
+ /** Must match the prefix given to the adapter. */
64
+ prefix?: string;
65
+ /**
66
+ * Schemas owned by plugins, exported alongside each plugin — `pushSchema`
67
+ * from `easy-ping/plugins/push`, for instance.
68
+ */
69
+ plugins?: readonly SchemaDeclaration[];
70
+ };
71
+ /**
72
+ * Creates the core tables, plus any plugin tables passed, and returns how many
73
+ * statements ran.
74
+ *
75
+ * Every statement is `IF NOT EXISTS`, so this is safe to run on every boot.
76
+ * It is a bootstrap, not a migration: it never alters a table that already
77
+ * exists. Once a database is live, use `planPostgresMigration` instead.
78
+ */
79
+ declare function createPostgresTables(query: SqlQuery, options?: CreateTablesOptions): Promise<number>;
80
+
81
+ export { type CreateTablesOptions, type PgPoolLike, type PostgresAdapterOptions, type SqlQuery, createPostgresTables, pgTransaction, postgresAdapter };
@@ -0,0 +1,360 @@
1
+ import { isOperator } from '../chunk-OZSOIX23.js';
2
+ import { decodeBase64Url, encodeBase64Url } from '../chunk-PTGHMCCG.js';
3
+ import { renderPostgresDdl } from '../chunk-RBDW4S6F.js';
4
+ import { toSnakeCase, coreSchema } from '../chunk-NYDVNMKG.js';
5
+
6
+ // src/adapters/postgres/statement.ts
7
+ var Statement = class {
8
+ #text = "";
9
+ params = [];
10
+ /** Raw SQL. Never pass user input through this — use `value`. */
11
+ raw(fragment) {
12
+ this.#text += fragment;
13
+ return this;
14
+ }
15
+ /** Binds a value and writes its placeholder. */
16
+ value(value) {
17
+ this.params.push(value instanceof Date ? value.toISOString() : value);
18
+ this.#text += `$${this.params.length}`;
19
+ return this;
20
+ }
21
+ /** `$1, $2, $3` — a comma-separated run of bound values. */
22
+ list(values, separator = ", ") {
23
+ values.forEach((value, index) => {
24
+ if (index > 0) this.raw(separator);
25
+ this.value(value);
26
+ });
27
+ return this;
28
+ }
29
+ /** `($1::text, $2::timestamptz)` — one VALUES tuple with explicit casts. */
30
+ tuple(cells) {
31
+ this.raw("(");
32
+ cells.forEach(([value, cast], index) => {
33
+ if (index > 0) this.raw(", ");
34
+ this.value(value).raw(`::${cast}`);
35
+ });
36
+ return this.raw(")");
37
+ }
38
+ get text() {
39
+ return this.#text;
40
+ }
41
+ };
42
+ var quote = (identifier) => `"${identifier.replace(/"/g, '""')}"`;
43
+
44
+ // src/adapters/postgres/adapter.ts
45
+ var str = (value) => String(value);
46
+ var nullableStr = (value) => value == null ? null : String(value);
47
+ var num = (value) => Number(value);
48
+ var date = (value) => value instanceof Date ? value : new Date(String(value));
49
+ var nullableDate = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
50
+ var encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
51
+ function decodeCursor(cursor) {
52
+ const decoded = decodeBase64Url(cursor);
53
+ if (!decoded) return null;
54
+ const [iso, id] = decoded.split("|");
55
+ if (!iso || !id) return null;
56
+ const createdAt = new Date(iso);
57
+ return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
58
+ }
59
+ function toNotification(row) {
60
+ return {
61
+ id: str(row.id),
62
+ userId: str(row.user_id),
63
+ type: str(row.type),
64
+ payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
65
+ actorId: nullableStr(row.actor_id),
66
+ groupKey: nullableStr(row.group_key),
67
+ dedupeKey: nullableStr(row.dedupe_key),
68
+ seenAt: nullableDate(row.seen_at),
69
+ readAt: nullableDate(row.read_at),
70
+ archivedAt: nullableDate(row.archived_at),
71
+ createdAt: date(row.created_at)
72
+ };
73
+ }
74
+ function toDelivery(row) {
75
+ return {
76
+ id: str(row.id),
77
+ notificationId: str(row.notification_id),
78
+ channel: str(row.channel),
79
+ status: str(row.status),
80
+ attempts: num(row.attempts),
81
+ maxAttempts: num(row.max_attempts),
82
+ notBefore: date(row.not_before),
83
+ claimedAt: nullableDate(row.claimed_at),
84
+ claimedBy: nullableStr(row.claimed_by),
85
+ lastError: nullableStr(row.last_error),
86
+ updatedAt: date(row.updated_at)
87
+ };
88
+ }
89
+ function appendWhere(statement, where) {
90
+ const entries = Object.entries(where);
91
+ if (entries.length === 0) return;
92
+ statement.raw(" WHERE ");
93
+ entries.forEach(([field, condition], index) => {
94
+ if (index > 0) statement.raw(" AND ");
95
+ const column = quote(toSnakeCase(field));
96
+ if (condition === null) {
97
+ statement.raw(`${column} IS NULL`);
98
+ return;
99
+ }
100
+ if (!isOperator(condition)) {
101
+ statement.raw(`${column} = `).value(condition);
102
+ return;
103
+ }
104
+ const operator = condition;
105
+ if ("in" in operator) {
106
+ const list = operator.in;
107
+ if (list.length === 0) statement.raw("FALSE");
108
+ else statement.raw(`${column} IN (`).list(list).raw(")");
109
+ return;
110
+ }
111
+ if ("lt" in operator) statement.raw(`${column} < `).value(operator.lt);
112
+ else if ("lte" in operator) statement.raw(`${column} <= `).value(operator.lte);
113
+ else if ("gt" in operator) statement.raw(`${column} > `).value(operator.gt);
114
+ else if ("gte" in operator) statement.raw(`${column} >= `).value(operator.gte);
115
+ else if ("not" in operator) {
116
+ if (operator.not === null) statement.raw(`${column} IS NOT NULL`);
117
+ else statement.raw(`${column} IS DISTINCT FROM `).value(operator.not);
118
+ } else statement.raw("TRUE");
119
+ });
120
+ }
121
+ function postgresAdapter(query, options = {}) {
122
+ const prefix = options.prefix ?? "";
123
+ const NOTIFICATION = quote(`${prefix}notification`);
124
+ const DELIVERY = quote(`${prefix}notification_delivery`);
125
+ const run = async (statement, exec = query) => exec(statement.text, statement.params);
126
+ const atomically = (fn) => options.transaction ? options.transaction(fn) : fn(query);
127
+ return {
128
+ name: "postgres",
129
+ naming: "snake_case",
130
+ serializesJson: true,
131
+ async createNotifications(input) {
132
+ if (input.length === 0) return { created: [], deduped: [] };
133
+ const now = /* @__PURE__ */ new Date();
134
+ return atomically(async (exec) => {
135
+ const insert = new Statement().raw(
136
+ `INSERT INTO ${NOTIFICATION} (id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES `
137
+ );
138
+ input.forEach((row, index) => {
139
+ if (index > 0) insert.raw(", ");
140
+ insert.tuple([
141
+ [row.id, "text"],
142
+ [row.userId, "text"],
143
+ [row.type, "text"],
144
+ [JSON.stringify(row.payload ?? null), "jsonb"],
145
+ [row.actorId ?? null, "text"],
146
+ [row.groupKey ?? null, "text"],
147
+ [row.dedupeKey ?? null, "text"],
148
+ [now, "timestamptz"]
149
+ ]);
150
+ });
151
+ insert.raw(" ON CONFLICT (user_id, dedupe_key) DO NOTHING RETURNING id");
152
+ const inserted = await run(insert, exec);
153
+ const created = new Set(inserted.map((row) => str(row.id)));
154
+ const deliveries = input.filter((row) => created.has(row.id)).flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));
155
+ if (deliveries.length > 0) {
156
+ const insertDeliveries = new Statement().raw(
157
+ `INSERT INTO ${DELIVERY} (id, notification_id, channel, max_attempts, not_before, updated_at) VALUES `
158
+ );
159
+ deliveries.forEach(({ row, delivery }, index) => {
160
+ if (index > 0) insertDeliveries.raw(", ");
161
+ insertDeliveries.tuple([
162
+ [delivery.id, "text"],
163
+ [row.id, "text"],
164
+ [delivery.channel, "text"],
165
+ [delivery.maxAttempts, "integer"],
166
+ [delivery.notBefore, "timestamptz"],
167
+ [now, "timestamptz"]
168
+ ]);
169
+ });
170
+ await run(insertDeliveries, exec);
171
+ }
172
+ return {
173
+ created: [...created],
174
+ deduped: input.filter((row) => !created.has(row.id)).map((row) => row.id)
175
+ };
176
+ });
177
+ },
178
+ async claimPendingDeliveries(args) {
179
+ const now = args.now ?? /* @__PURE__ */ new Date();
180
+ const staleBefore = new Date(now.getTime() - args.leaseMs);
181
+ 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");
182
+ if (args.channels && args.channels.length > 0) {
183
+ statement.raw(" AND channel IN (").list(args.channels).raw(")");
184
+ }
185
+ if (args.ids && args.ids.length > 0) {
186
+ statement.raw(" AND id IN (").list(args.ids).raw(")");
187
+ }
188
+ 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(
189
+ ` 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`
190
+ );
191
+ const rows = await run(statement);
192
+ return rows.map((row) => ({
193
+ id: str(row.id),
194
+ notificationId: str(row.notification_id),
195
+ channel: str(row.channel),
196
+ attempts: num(row.attempts),
197
+ maxAttempts: num(row.max_attempts),
198
+ notification: {
199
+ userId: str(row.user_id),
200
+ type: str(row.type),
201
+ payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
202
+ actorId: nullableStr(row.actor_id)
203
+ }
204
+ }));
205
+ },
206
+ async releaseDeliveries(releases) {
207
+ if (releases.length === 0) return;
208
+ const now = /* @__PURE__ */ new Date();
209
+ const statement = new Statement().raw(
210
+ `UPDATE ${DELIVERY} d SET status = CASE WHEN v.result = 'sent' THEN 'sent' WHEN v.result = 'skipped' THEN 'skipped' WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending' ELSE 'failed' END, attempts = CASE WHEN v.result IN ('sent', 'skipped') 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 = `
211
+ );
212
+ statement.value(now).raw("::timestamptz FROM (VALUES ");
213
+ releases.forEach(({ id, outcome, nextAttemptAt }, index) => {
214
+ if (index > 0) statement.raw(", ");
215
+ statement.tuple([
216
+ [id, "text"],
217
+ [outcome.result, "text"],
218
+ [outcome.result === "failed" && outcome.retryable, "boolean"],
219
+ [
220
+ outcome.result === "failed" ? outcome.error.slice(0, 2e3) : outcome.result === "skipped" ? outcome.reason.slice(0, 2e3) : null,
221
+ "text"
222
+ ],
223
+ [nextAttemptAt ?? null, "timestamptz"]
224
+ ]);
225
+ });
226
+ statement.raw(") AS v(id, result, retryable, error, not_before) WHERE d.id = v.id");
227
+ await run(statement);
228
+ },
229
+ async listNotifications(feed) {
230
+ const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;
231
+ const statement = new Statement().raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `).value(feed.userId).raw("::text AND archived_at IS NULL");
232
+ if (feed.unreadOnly) statement.raw(" AND read_at IS NULL");
233
+ if (cursor) {
234
+ statement.raw(" AND (created_at, id) < (").value(cursor.createdAt).raw("::timestamptz, ").value(cursor.id).raw("::text)");
235
+ }
236
+ statement.raw(" ORDER BY created_at DESC, id DESC LIMIT ").value(feed.limit + 1);
237
+ const rows = await run(statement);
238
+ const page = rows.slice(0, feed.limit).map(toNotification);
239
+ const last = page.at(-1);
240
+ return {
241
+ notifications: page,
242
+ nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null
243
+ };
244
+ },
245
+ async countUnseen(userId) {
246
+ const rows = await run(
247
+ 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")
248
+ );
249
+ return num(rows[0]?.count ?? 0);
250
+ },
251
+ async markSeen(userId, before) {
252
+ await run(
253
+ 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")
254
+ );
255
+ },
256
+ async markRead(userId, notificationIds) {
257
+ if (notificationIds.length === 0) return 0;
258
+ const rows = await run(
259
+ 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")
260
+ );
261
+ return rows.length;
262
+ },
263
+ async markAllRead(userId) {
264
+ const rows = await run(
265
+ 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")
266
+ );
267
+ return rows.length;
268
+ },
269
+ async getFailedDeliveries(args) {
270
+ const rows = await run(
271
+ 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)
272
+ );
273
+ return rows.map(toDelivery);
274
+ },
275
+ async queryTable(table, where, queryOptions) {
276
+ const statement = new Statement().raw(`SELECT * FROM ${quote(table)}`);
277
+ appendWhere(statement, where);
278
+ if (queryOptions.orderBy) {
279
+ const direction = queryOptions.orderBy.direction === "desc" ? "DESC" : "ASC";
280
+ statement.raw(` ORDER BY ${quote(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);
281
+ }
282
+ if (queryOptions.limit) statement.raw(" LIMIT ").value(queryOptions.limit);
283
+ return [...await run(statement)];
284
+ },
285
+ async insertRows(table, rows, onConflict) {
286
+ if (rows.length === 0) return 0;
287
+ const columns = Object.keys(rows[0] ?? {});
288
+ if (columns.length === 0) return 0;
289
+ const statement = new Statement().raw(
290
+ `INSERT INTO ${quote(table)} (${columns.map((c) => quote(toSnakeCase(c))).join(", ")}) VALUES `
291
+ );
292
+ rows.forEach((row, index) => {
293
+ if (index > 0) statement.raw(", ");
294
+ statement.raw("(").list(columns.map((column) => row[column])).raw(")");
295
+ });
296
+ if (onConflict && onConflict.length > 0) {
297
+ const assignments = columns.filter((column) => !onConflict.includes(column)).map((column) => {
298
+ const quoted = quote(toSnakeCase(column));
299
+ return `${quoted} = EXCLUDED.${quoted}`;
300
+ });
301
+ statement.raw(
302
+ ` ON CONFLICT (${onConflict.map((f) => quote(toSnakeCase(f))).join(", ")}) DO UPDATE SET ${assignments.join(", ")}`
303
+ );
304
+ }
305
+ statement.raw(" RETURNING 1 AS ok");
306
+ return (await run(statement)).length;
307
+ },
308
+ async updateRows(table, where, set) {
309
+ const assignments = Object.entries(set);
310
+ if (assignments.length === 0) return 0;
311
+ const statement = new Statement().raw(`UPDATE ${quote(table)} SET `);
312
+ assignments.forEach(([field, value], index) => {
313
+ if (index > 0) statement.raw(", ");
314
+ statement.raw(`${quote(toSnakeCase(field))} = `).value(value);
315
+ });
316
+ appendWhere(statement, where);
317
+ statement.raw(" RETURNING 1 AS ok");
318
+ return (await run(statement)).length;
319
+ },
320
+ async deleteRows(table, where) {
321
+ const statement = new Statement().raw(`DELETE FROM ${quote(table)}`);
322
+ appendWhere(statement, where);
323
+ statement.raw(" RETURNING 1 AS ok");
324
+ return (await run(statement)).length;
325
+ }
326
+ };
327
+ }
328
+
329
+ // src/adapters/postgres/helpers.ts
330
+ function pgTransaction(pool) {
331
+ return async (fn) => {
332
+ const client = await pool.connect();
333
+ try {
334
+ await client.query("BEGIN");
335
+ const result = await fn(async (text, params) => (await client.query(text, params)).rows);
336
+ await client.query("COMMIT");
337
+ return result;
338
+ } catch (error) {
339
+ await client.query("ROLLBACK");
340
+ throw error;
341
+ } finally {
342
+ client.release();
343
+ }
344
+ };
345
+ }
346
+ async function createPostgresTables(query, options = {}) {
347
+ const schemas = [coreSchema, ...options.plugins ?? []];
348
+ let count = 0;
349
+ for (const schema of schemas) {
350
+ for (const statement of renderPostgresDdl(schema, options.prefix)) {
351
+ await query(statement, []);
352
+ count += 1;
353
+ }
354
+ }
355
+ return count;
356
+ }
357
+
358
+ export { createPostgresTables, pgTransaction, postgresAdapter };
359
+ //# sourceMappingURL=postgres.js.map
360
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/postgres/statement.ts","../../src/adapters/postgres/adapter.ts","../../src/adapters/postgres/helpers.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.result = 'skipped' THEN 'skipped'\" +\n \" WHEN v.retryable AND d.attempts + 1 < d.max_attempts THEN 'pending'\" +\n \" ELSE 'failed' END,\" +\n \" attempts = CASE WHEN v.result IN ('sent', 'skipped')\" +\n \" 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 [\n outcome.result === \"failed\"\n ? outcome.error.slice(0, 2000)\n : outcome.result === \"skipped\"\n ? outcome.reason.slice(0, 2000)\n : null,\n \"text\",\n ],\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","import type { SchemaDeclaration } from \"../../core/plugin\";\nimport { coreSchema, renderPostgresDdl } from \"../../schema\";\nimport type { SqlQuery } from \"./adapter\";\n\n/**\n * The slice of `pg`'s Pool this needs, declared structurally so the library\n * takes no dependency on the driver. `postgres.js` and Kysely have their own\n * transaction helpers and do not need this one.\n */\nexport type PgPoolLike = {\n connect(): Promise<{\n query(text: string, params?: readonly unknown[]): Promise<{ rows: Record<string, unknown>[] }>;\n release(): void;\n }>;\n};\n\n/**\n * The `transaction` option for `node-postgres`, so callers stop hand-writing\n * the same sixteen lines of BEGIN/COMMIT/ROLLBACK/release.\n *\n * postgresAdapter(query, { transaction: pgTransaction(pool) })\n *\n * The `finally` is the part worth having in one place: drop the release and\n * every send leaks a connection until the pool is exhausted.\n */\nexport function pgTransaction(pool: PgPoolLike) {\n return async <T>(fn: (query: SqlQuery) => Promise<T>): Promise<T> => {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n const result = await fn(async (text, params) => (await client.query(text, params)).rows);\n await client.query(\"COMMIT\");\n return result;\n } catch (error) {\n await client.query(\"ROLLBACK\");\n throw error;\n } finally {\n client.release();\n }\n };\n}\n\nexport type CreateTablesOptions = {\n /** Must match the prefix given to the adapter. */\n prefix?: string;\n /**\n * Schemas owned by plugins, exported alongside each plugin — `pushSchema`\n * from `easy-ping/plugins/push`, for instance.\n */\n plugins?: readonly SchemaDeclaration[];\n};\n\n/**\n * Creates the core tables, plus any plugin tables passed, and returns how many\n * statements ran.\n *\n * Every statement is `IF NOT EXISTS`, so this is safe to run on every boot.\n * It is a bootstrap, not a migration: it never alters a table that already\n * exists. Once a database is live, use `planPostgresMigration` instead.\n */\nexport async function createPostgresTables(\n query: SqlQuery,\n options: CreateTablesOptions = {},\n): Promise<number> {\n const schemas: readonly SchemaDeclaration[] = [coreSchema, ...(options.plugins ?? [])];\n\n let count = 0;\n for (const schema of schemas) {\n for (const statement of renderPostgresDdl(schema, options.prefix)) {\n await query(statement, []);\n count += 1;\n }\n }\n return count;\n}\n"]}