routeflow-api 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.
Files changed (70) hide show
  1. package/README.md +93 -0
  2. package/dist/adapters/cassandra.cjs +117 -0
  3. package/dist/adapters/cassandra.cjs.map +1 -0
  4. package/dist/adapters/cassandra.d.cts +37 -0
  5. package/dist/adapters/cassandra.d.ts +37 -0
  6. package/dist/adapters/cassandra.js +90 -0
  7. package/dist/adapters/cassandra.js.map +1 -0
  8. package/dist/adapters/dynamodb.cjs +180 -0
  9. package/dist/adapters/dynamodb.cjs.map +1 -0
  10. package/dist/adapters/dynamodb.d.cts +48 -0
  11. package/dist/adapters/dynamodb.d.ts +48 -0
  12. package/dist/adapters/dynamodb.js +153 -0
  13. package/dist/adapters/dynamodb.js.map +1 -0
  14. package/dist/adapters/elasticsearch.cjs +120 -0
  15. package/dist/adapters/elasticsearch.cjs.map +1 -0
  16. package/dist/adapters/elasticsearch.d.cts +43 -0
  17. package/dist/adapters/elasticsearch.d.ts +43 -0
  18. package/dist/adapters/elasticsearch.js +93 -0
  19. package/dist/adapters/elasticsearch.js.map +1 -0
  20. package/dist/adapters/mongodb.cjs +159 -0
  21. package/dist/adapters/mongodb.cjs.map +1 -0
  22. package/dist/adapters/mongodb.d.cts +54 -0
  23. package/dist/adapters/mongodb.d.ts +54 -0
  24. package/dist/adapters/mongodb.js +132 -0
  25. package/dist/adapters/mongodb.js.map +1 -0
  26. package/dist/adapters/mysql.cjs +159 -0
  27. package/dist/adapters/mysql.cjs.map +1 -0
  28. package/dist/adapters/mysql.d.cts +63 -0
  29. package/dist/adapters/mysql.d.ts +63 -0
  30. package/dist/adapters/mysql.js +132 -0
  31. package/dist/adapters/mysql.js.map +1 -0
  32. package/dist/adapters/opensearch.cjs +120 -0
  33. package/dist/adapters/opensearch.cjs.map +1 -0
  34. package/dist/adapters/opensearch.d.cts +2 -0
  35. package/dist/adapters/opensearch.d.ts +2 -0
  36. package/dist/adapters/opensearch.js +93 -0
  37. package/dist/adapters/opensearch.js.map +1 -0
  38. package/dist/adapters/postgres.cjs +271 -0
  39. package/dist/adapters/postgres.cjs.map +1 -0
  40. package/dist/adapters/postgres.d.cts +81 -0
  41. package/dist/adapters/postgres.d.ts +81 -0
  42. package/dist/adapters/postgres.js +244 -0
  43. package/dist/adapters/postgres.js.map +1 -0
  44. package/dist/adapters/redis.cjs +153 -0
  45. package/dist/adapters/redis.cjs.map +1 -0
  46. package/dist/adapters/redis.d.cts +40 -0
  47. package/dist/adapters/redis.d.ts +40 -0
  48. package/dist/adapters/redis.js +126 -0
  49. package/dist/adapters/redis.js.map +1 -0
  50. package/dist/adapters/snowflake.cjs +117 -0
  51. package/dist/adapters/snowflake.cjs.map +1 -0
  52. package/dist/adapters/snowflake.d.cts +37 -0
  53. package/dist/adapters/snowflake.d.ts +37 -0
  54. package/dist/adapters/snowflake.js +90 -0
  55. package/dist/adapters/snowflake.js.map +1 -0
  56. package/dist/client/index.cjs +484 -0
  57. package/dist/client/index.cjs.map +1 -0
  58. package/dist/client/index.d.cts +174 -0
  59. package/dist/client/index.d.ts +174 -0
  60. package/dist/client/index.js +455 -0
  61. package/dist/client/index.js.map +1 -0
  62. package/dist/index.cjs +935 -0
  63. package/dist/index.cjs.map +1 -0
  64. package/dist/index.d.cts +190 -0
  65. package/dist/index.d.ts +190 -0
  66. package/dist/index.js +890 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/types-tPDla8AE.d.cts +75 -0
  69. package/dist/types-tPDla8AE.d.ts +75 -0
  70. package/package.json +157 -0
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/postgres.ts
21
+ var postgres_exports = {};
22
+ __export(postgres_exports, {
23
+ PostgresAdapter: () => PostgresAdapter
24
+ });
25
+ module.exports = __toCommonJS(postgres_exports);
26
+
27
+ // src/adapters/postgres/postgres-adapter.ts
28
+ var import_pg = require("pg");
29
+
30
+ // src/core/errors.ts
31
+ var ReactiveApiError = class extends Error {
32
+ /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
33
+ code;
34
+ constructor(code, message) {
35
+ super(message);
36
+ this.name = "ReactiveApiError";
37
+ this.code = code;
38
+ Object.setPrototypeOf(this, new.target.prototype);
39
+ }
40
+ };
41
+
42
+ // src/adapters/postgres/trigger-sql.ts
43
+ function notifyChannel(prefix) {
44
+ return `${prefix}_changes`;
45
+ }
46
+ function createTriggerFunctionSQL(schema, prefix) {
47
+ const fnName = `${schema}.${prefix}_notify_changes`;
48
+ const channel = notifyChannel(prefix);
49
+ return `
50
+ CREATE OR REPLACE FUNCTION ${fnName}()
51
+ RETURNS trigger
52
+ LANGUAGE plpgsql
53
+ AS $$
54
+ DECLARE
55
+ payload TEXT;
56
+ new_row JSON;
57
+ old_row JSON;
58
+ BEGIN
59
+ IF (TG_OP = 'DELETE') THEN
60
+ new_row := NULL;
61
+ old_row := row_to_json(OLD);
62
+ ELSIF (TG_OP = 'INSERT') THEN
63
+ new_row := row_to_json(NEW);
64
+ old_row := NULL;
65
+ ELSE
66
+ new_row := row_to_json(NEW);
67
+ old_row := row_to_json(OLD);
68
+ END IF;
69
+
70
+ payload := json_build_object(
71
+ 'table', TG_TABLE_NAME,
72
+ 'operation', TG_OP,
73
+ 'new_row', new_row,
74
+ 'old_row', old_row
75
+ )::text;
76
+
77
+ -- Truncate gracefully if payload exceeds pg's 8000-byte NOTIFY limit
78
+ IF octet_length(payload) > 7900 THEN
79
+ payload := json_build_object(
80
+ 'table', TG_TABLE_NAME,
81
+ 'operation', TG_OP,
82
+ 'new_row', NULL,
83
+ 'old_row', NULL,
84
+ '_truncated', true
85
+ )::text;
86
+ END IF;
87
+
88
+ PERFORM pg_notify('${channel}', payload);
89
+ RETURN COALESCE(NEW, OLD);
90
+ END;
91
+ $$;
92
+ `.trim();
93
+ }
94
+ function createTableTriggerSQL(schema, prefix, table) {
95
+ const triggerName = `${prefix}_notify_${table}`;
96
+ const fnName = `${schema}.${prefix}_notify_changes`;
97
+ return `
98
+ DO $$
99
+ BEGIN
100
+ IF NOT EXISTS (
101
+ SELECT 1 FROM pg_trigger
102
+ WHERE tgname = '${triggerName}'
103
+ AND tgrelid = '${schema}.${table}'::regclass
104
+ ) THEN
105
+ CREATE TRIGGER ${triggerName}
106
+ AFTER INSERT OR UPDATE OR DELETE ON ${schema}.${table}
107
+ FOR EACH ROW EXECUTE FUNCTION ${fnName}();
108
+ END IF;
109
+ END;
110
+ $$;
111
+ `.trim();
112
+ }
113
+ function dropTableTriggerSQL(schema, prefix, table) {
114
+ const triggerName = `${prefix}_notify_${table}`;
115
+ return `DROP TRIGGER IF EXISTS ${triggerName} ON ${schema}.${table};`;
116
+ }
117
+ function dropTriggerFunctionSQL(schema, prefix) {
118
+ return `DROP FUNCTION IF EXISTS ${schema}.${prefix}_notify_changes();`;
119
+ }
120
+
121
+ // src/adapters/postgres/postgres-adapter.ts
122
+ var PostgresAdapter = class {
123
+ listenClient = null;
124
+ schema;
125
+ prefix;
126
+ connectionString;
127
+ /** table → Set<callback> */
128
+ listeners = /* @__PURE__ */ new Map();
129
+ /** Tables for which a trigger has already been installed */
130
+ installedTriggers = /* @__PURE__ */ new Set();
131
+ constructor(options) {
132
+ this.connectionString = options.connectionString;
133
+ this.schema = options.schema ?? "public";
134
+ this.prefix = options.triggerPrefix ?? "reactive_api";
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // DatabaseAdapter interface
138
+ // ---------------------------------------------------------------------------
139
+ /** Connect the LISTEN client and install the shared trigger function. */
140
+ async connect() {
141
+ if (this.listenClient) return;
142
+ const client = new import_pg.Client({ connectionString: this.connectionString });
143
+ try {
144
+ await client.connect();
145
+ } catch (err) {
146
+ throw new ReactiveApiError(
147
+ "POSTGRES_CONNECT_FAILED",
148
+ `Failed to connect to PostgreSQL: ${errorMessage(err)}`
149
+ );
150
+ }
151
+ try {
152
+ await client.query(createTriggerFunctionSQL(this.schema, this.prefix));
153
+ } catch (err) {
154
+ await client.end().catch(() => void 0);
155
+ throw new ReactiveApiError(
156
+ "POSTGRES_TRIGGER_FUNCTION_FAILED",
157
+ `Failed to install trigger function: ${errorMessage(err)}`
158
+ );
159
+ }
160
+ const channel = notifyChannel(this.prefix);
161
+ await client.query(`LISTEN "${channel}"`);
162
+ client.on("notification", (msg) => {
163
+ if (msg.channel !== channel || !msg.payload) return;
164
+ this.handleNotification(msg.payload);
165
+ });
166
+ client.on("error", (err) => {
167
+ console.error("[RouteFlow/postgres] LISTEN client error:", err.message);
168
+ });
169
+ this.listenClient = client;
170
+ }
171
+ /** Disconnect the LISTEN client and optionally clean up triggers. */
172
+ async disconnect() {
173
+ if (!this.listenClient) return;
174
+ const client = this.listenClient;
175
+ this.listenClient = null;
176
+ for (const table of this.installedTriggers) {
177
+ await client.query(dropTableTriggerSQL(this.schema, this.prefix, table)).catch(() => void 0);
178
+ }
179
+ this.installedTriggers.clear();
180
+ await client.query(dropTriggerFunctionSQL(this.schema, this.prefix)).catch(() => void 0);
181
+ await client.end().catch(() => void 0);
182
+ this.listeners.clear();
183
+ }
184
+ /**
185
+ * Register a callback for changes on `table`.
186
+ * Installs a PostgreSQL trigger on first registration for this table.
187
+ *
188
+ * @returns An unsubscribe function.
189
+ */
190
+ onChange(table, callback) {
191
+ if (!this.listeners.has(table)) {
192
+ this.listeners.set(table, /* @__PURE__ */ new Set());
193
+ }
194
+ this.listeners.get(table).add(callback);
195
+ this.ensureTriggerInstalled(table).catch((err) => {
196
+ console.error(
197
+ `[RouteFlow/postgres] Failed to install trigger for table "${table}":`,
198
+ errorMessage(err)
199
+ );
200
+ });
201
+ return () => {
202
+ this.listeners.get(table)?.delete(callback);
203
+ };
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Private helpers
207
+ // ---------------------------------------------------------------------------
208
+ async ensureTriggerInstalled(table) {
209
+ if (this.installedTriggers.has(table)) return;
210
+ if (!this.listenClient) return;
211
+ try {
212
+ await this.listenClient.query(
213
+ createTableTriggerSQL(this.schema, this.prefix, table)
214
+ );
215
+ this.installedTriggers.add(table);
216
+ } catch (err) {
217
+ if (err instanceof import_pg.DatabaseError) {
218
+ throw new ReactiveApiError(
219
+ "POSTGRES_TRIGGER_INSTALL_FAILED",
220
+ `Failed to install trigger on "${table}": ${err.message}`
221
+ );
222
+ }
223
+ throw err;
224
+ }
225
+ }
226
+ handleNotification(rawPayload) {
227
+ let payload;
228
+ try {
229
+ payload = JSON.parse(rawPayload);
230
+ } catch {
231
+ console.error("[RouteFlow/postgres] Malformed NOTIFY payload:", rawPayload);
232
+ return;
233
+ }
234
+ if (!isNotifyPayload(payload)) {
235
+ console.error("[RouteFlow/postgres] Unexpected payload shape:", payload);
236
+ return;
237
+ }
238
+ const event = {
239
+ table: payload.table,
240
+ operation: payload.operation,
241
+ newRow: payload.new_row,
242
+ oldRow: payload.old_row,
243
+ timestamp: Date.now()
244
+ };
245
+ const callbacks = this.listeners.get(payload.table);
246
+ if (!callbacks) return;
247
+ for (const cb of callbacks) {
248
+ try {
249
+ cb(event);
250
+ } catch (err) {
251
+ console.error(
252
+ `[RouteFlow/postgres] Listener error on table "${payload.table}":`,
253
+ errorMessage(err)
254
+ );
255
+ }
256
+ }
257
+ }
258
+ };
259
+ function isNotifyPayload(value) {
260
+ if (typeof value !== "object" || value === null) return false;
261
+ const v = value;
262
+ return typeof v["table"] === "string" && (v["operation"] === "INSERT" || v["operation"] === "UPDATE" || v["operation"] === "DELETE");
263
+ }
264
+ function errorMessage(err) {
265
+ return err instanceof Error ? err.message : String(err);
266
+ }
267
+ // Annotate the CommonJS export names for ESM import in node:
268
+ 0 && (module.exports = {
269
+ PostgresAdapter
270
+ });
271
+ //# sourceMappingURL=postgres.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/postgres.ts","../../src/adapters/postgres/postgres-adapter.ts","../../src/core/errors.ts","../../src/adapters/postgres/trigger-sql.ts"],"sourcesContent":["export { PostgresAdapter } from './postgres/postgres-adapter.js'\nexport type { PostgresAdapterOptions, NotifyPayload } from './postgres/types.js'\n","import { Client, DatabaseError } from 'pg'\nimport type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type { NotifyPayload, PostgresAdapterOptions } from './types.js'\nimport {\n notifyChannel,\n createTriggerFunctionSQL,\n createTableTriggerSQL,\n dropTableTriggerSQL,\n dropTriggerFunctionSQL,\n} from './trigger-sql.js'\n\n/**\n * PostgreSQL database adapter for RouteFlow.\n *\n * Uses a dedicated `pg.Client` (not a pool) for LISTEN so the connection\n * stays open and notifications are never missed. A separate pool/client\n * can still be used by the application for regular queries.\n *\n * Change detection mechanism:\n * - On `onChange(table)`: installs a per-table AFTER INSERT/UPDATE/DELETE trigger\n * that calls `pg_notify` with a JSON payload.\n * - A single LISTEN connection subscribes to one shared channel for all tables.\n * - Payloads > 7900 bytes are sent without row data (`_truncated: true`);\n * subscribers receive a ChangeEvent with `newRow: null` / `oldRow: null`.\n *\n * @example\n * ```ts\n * const adapter = new PostgresAdapter({\n * connectionString: process.env.DATABASE_URL,\n * })\n * await adapter.connect()\n * ```\n */\nexport class PostgresAdapter implements DatabaseAdapter {\n private listenClient: Client | null = null\n private readonly schema: string\n private readonly prefix: string\n private readonly connectionString: string\n\n /** table → Set<callback> */\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n /** Tables for which a trigger has already been installed */\n private readonly installedTriggers: Set<string> = new Set()\n\n constructor(options: PostgresAdapterOptions) {\n this.connectionString = options.connectionString\n this.schema = options.schema ?? 'public'\n this.prefix = options.triggerPrefix ?? 'reactive_api'\n }\n\n // ---------------------------------------------------------------------------\n // DatabaseAdapter interface\n // ---------------------------------------------------------------------------\n\n /** Connect the LISTEN client and install the shared trigger function. */\n async connect(): Promise<void> {\n if (this.listenClient) return\n\n const client = new Client({ connectionString: this.connectionString })\n try {\n await client.connect()\n } catch (err) {\n throw new ReactiveApiError(\n 'POSTGRES_CONNECT_FAILED',\n `Failed to connect to PostgreSQL: ${errorMessage(err)}`,\n )\n }\n\n // Install (or replace) the shared trigger function once per connection\n try {\n await client.query(createTriggerFunctionSQL(this.schema, this.prefix))\n } catch (err) {\n await client.end().catch(() => undefined)\n throw new ReactiveApiError(\n 'POSTGRES_TRIGGER_FUNCTION_FAILED',\n `Failed to install trigger function: ${errorMessage(err)}`,\n )\n }\n\n // Start listening on the shared channel\n const channel = notifyChannel(this.prefix)\n await client.query(`LISTEN \"${channel}\"`)\n\n client.on('notification', (msg) => {\n if (msg.channel !== channel || !msg.payload) return\n this.handleNotification(msg.payload)\n })\n\n client.on('error', (err) => {\n // Log but don't crash — the adapter becomes unavailable; the app can\n // call disconnect() + connect() to recover.\n console.error('[RouteFlow/postgres] LISTEN client error:', err.message)\n })\n\n this.listenClient = client\n }\n\n /** Disconnect the LISTEN client and optionally clean up triggers. */\n async disconnect(): Promise<void> {\n if (!this.listenClient) return\n\n const client = this.listenClient\n this.listenClient = null\n\n // Remove all installed table triggers and the shared function\n for (const table of this.installedTriggers) {\n await client\n .query(dropTableTriggerSQL(this.schema, this.prefix, table))\n .catch(() => undefined)\n }\n this.installedTriggers.clear()\n\n await client\n .query(dropTriggerFunctionSQL(this.schema, this.prefix))\n .catch(() => undefined)\n\n await client.end().catch(() => undefined)\n this.listeners.clear()\n }\n\n /**\n * Register a callback for changes on `table`.\n * Installs a PostgreSQL trigger on first registration for this table.\n *\n * @returns An unsubscribe function.\n */\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) {\n this.listeners.set(table, new Set())\n }\n this.listeners.get(table)!.add(callback)\n\n // Install trigger asynchronously; errors are surfaced to the console\n // rather than throwing so registration can happen before connect().\n this.ensureTriggerInstalled(table).catch((err) => {\n console.error(\n `[RouteFlow/postgres] Failed to install trigger for table \"${table}\":`,\n errorMessage(err),\n )\n })\n\n return () => {\n this.listeners.get(table)?.delete(callback)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private async ensureTriggerInstalled(table: string): Promise<void> {\n if (this.installedTriggers.has(table)) return\n if (!this.listenClient) return // will be retried via connect() flow\n\n try {\n await this.listenClient.query(\n createTableTriggerSQL(this.schema, this.prefix, table),\n )\n this.installedTriggers.add(table)\n } catch (err) {\n if (err instanceof DatabaseError) {\n throw new ReactiveApiError(\n 'POSTGRES_TRIGGER_INSTALL_FAILED',\n `Failed to install trigger on \"${table}\": ${err.message}`,\n )\n }\n throw err\n }\n }\n\n private handleNotification(rawPayload: string): void {\n let payload: unknown\n try {\n payload = JSON.parse(rawPayload)\n } catch {\n console.error('[RouteFlow/postgres] Malformed NOTIFY payload:', rawPayload)\n return\n }\n\n if (!isNotifyPayload(payload)) {\n console.error('[RouteFlow/postgres] Unexpected payload shape:', payload)\n return\n }\n\n const event: ChangeEvent = {\n table: payload.table,\n operation: payload.operation,\n newRow: payload.new_row,\n oldRow: payload.old_row,\n timestamp: Date.now(),\n }\n\n const callbacks = this.listeners.get(payload.table)\n if (!callbacks) return\n\n for (const cb of callbacks) {\n try {\n cb(event)\n } catch (err) {\n console.error(\n `[RouteFlow/postgres] Listener error on table \"${payload.table}\":`,\n errorMessage(err),\n )\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Type guards / utilities\n// ---------------------------------------------------------------------------\n\nfunction isNotifyPayload(value: unknown): value is NotifyPayload {\n if (typeof value !== 'object' || value === null) return false\n const v = value as Record<string, unknown>\n return (\n typeof v['table'] === 'string' &&\n (v['operation'] === 'INSERT' || v['operation'] === 'UPDATE' || v['operation'] === 'DELETE')\n )\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","/**\n * Generates the SQL that installs a per-table NOTIFY trigger.\n *\n * Strategy:\n * - One shared trigger function per (schema, prefix) pair — handles all tables.\n * - One trigger per table that calls that function.\n *\n * Payload sent via NOTIFY (channel = triggerPrefix):\n * ```json\n * { \"table\": \"orders\", \"operation\": \"INSERT\", \"new_row\": {...}, \"old_row\": null }\n * ```\n *\n * IMPORTANT: PostgreSQL NOTIFY payloads are limited to 8000 bytes.\n * For larger rows the payload is truncated and only the primary-key columns\n * are guaranteed to be present. Callers that need the full row should\n * re-query after receiving the event.\n */\n\n/** Channel name used for LISTEN/NOTIFY — one shared channel for all tables. */\nexport function notifyChannel(prefix: string): string {\n return `${prefix}_changes`\n}\n\n/**\n * SQL to create (or replace) the shared trigger function.\n * The function encodes NEW/OLD as JSON and notifies the channel.\n */\nexport function createTriggerFunctionSQL(schema: string, prefix: string): string {\n const fnName = `${schema}.${prefix}_notify_changes`\n const channel = notifyChannel(prefix)\n\n return `\nCREATE OR REPLACE FUNCTION ${fnName}()\nRETURNS trigger\nLANGUAGE plpgsql\nAS $$\nDECLARE\n payload TEXT;\n new_row JSON;\n old_row JSON;\nBEGIN\n IF (TG_OP = 'DELETE') THEN\n new_row := NULL;\n old_row := row_to_json(OLD);\n ELSIF (TG_OP = 'INSERT') THEN\n new_row := row_to_json(NEW);\n old_row := NULL;\n ELSE\n new_row := row_to_json(NEW);\n old_row := row_to_json(OLD);\n END IF;\n\n payload := json_build_object(\n 'table', TG_TABLE_NAME,\n 'operation', TG_OP,\n 'new_row', new_row,\n 'old_row', old_row\n )::text;\n\n -- Truncate gracefully if payload exceeds pg's 8000-byte NOTIFY limit\n IF octet_length(payload) > 7900 THEN\n payload := json_build_object(\n 'table', TG_TABLE_NAME,\n 'operation', TG_OP,\n 'new_row', NULL,\n 'old_row', NULL,\n '_truncated', true\n )::text;\n END IF;\n\n PERFORM pg_notify('${channel}', payload);\n RETURN COALESCE(NEW, OLD);\nEND;\n$$;\n`.trim()\n}\n\n/**\n * SQL to attach the trigger to a specific table.\n * Idempotent — uses CREATE OR REPLACE (Postgres 14+).\n * Falls back to DROP + CREATE for older versions.\n */\nexport function createTableTriggerSQL(\n schema: string,\n prefix: string,\n table: string,\n): string {\n const triggerName = `${prefix}_notify_${table}`\n const fnName = `${schema}.${prefix}_notify_changes`\n\n return `\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_trigger\n WHERE tgname = '${triggerName}'\n AND tgrelid = '${schema}.${table}'::regclass\n ) THEN\n CREATE TRIGGER ${triggerName}\n AFTER INSERT OR UPDATE OR DELETE ON ${schema}.${table}\n FOR EACH ROW EXECUTE FUNCTION ${fnName}();\n END IF;\nEND;\n$$;\n`.trim()\n}\n\n/**\n * SQL to drop the trigger from a table (used on disconnect/cleanup).\n */\nexport function dropTableTriggerSQL(\n schema: string,\n prefix: string,\n table: string,\n): string {\n const triggerName = `${prefix}_notify_${table}`\n return `DROP TRIGGER IF EXISTS ${triggerName} ON ${schema}.${table};`\n}\n\n/**\n * SQL to drop the shared trigger function (used on full adapter teardown).\n */\nexport function dropTriggerFunctionSQL(schema: string, prefix: string): string {\n return `DROP FUNCTION IF EXISTS ${schema}.${prefix}_notify_changes();`\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from '../types-tPDla8AE.cjs';
2
+
3
+ /**
4
+ * Configuration options for PostgresAdapter.
5
+ */
6
+ interface PostgresAdapterOptions {
7
+ /**
8
+ * PostgreSQL connection string.
9
+ * e.g. 'postgresql://user:password@localhost:5432/dbname'
10
+ */
11
+ connectionString: string;
12
+ /**
13
+ * Schema to install trigger functions in.
14
+ * Defaults to 'public'.
15
+ */
16
+ schema?: string;
17
+ /**
18
+ * Name prefix for generated trigger functions and triggers.
19
+ * Defaults to 'reactive_api'.
20
+ */
21
+ triggerPrefix?: string;
22
+ }
23
+ /**
24
+ * The raw JSON payload delivered by the PostgreSQL NOTIFY.
25
+ * Must stay in sync with the trigger function SQL in trigger-sql.ts.
26
+ */
27
+ interface NotifyPayload {
28
+ table: string;
29
+ operation: 'INSERT' | 'UPDATE' | 'DELETE';
30
+ new_row: Record<string, unknown> | null;
31
+ old_row: Record<string, unknown> | null;
32
+ }
33
+
34
+ /**
35
+ * PostgreSQL database adapter for RouteFlow.
36
+ *
37
+ * Uses a dedicated `pg.Client` (not a pool) for LISTEN so the connection
38
+ * stays open and notifications are never missed. A separate pool/client
39
+ * can still be used by the application for regular queries.
40
+ *
41
+ * Change detection mechanism:
42
+ * - On `onChange(table)`: installs a per-table AFTER INSERT/UPDATE/DELETE trigger
43
+ * that calls `pg_notify` with a JSON payload.
44
+ * - A single LISTEN connection subscribes to one shared channel for all tables.
45
+ * - Payloads > 7900 bytes are sent without row data (`_truncated: true`);
46
+ * subscribers receive a ChangeEvent with `newRow: null` / `oldRow: null`.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const adapter = new PostgresAdapter({
51
+ * connectionString: process.env.DATABASE_URL,
52
+ * })
53
+ * await adapter.connect()
54
+ * ```
55
+ */
56
+ declare class PostgresAdapter implements DatabaseAdapter {
57
+ private listenClient;
58
+ private readonly schema;
59
+ private readonly prefix;
60
+ private readonly connectionString;
61
+ /** table → Set<callback> */
62
+ private readonly listeners;
63
+ /** Tables for which a trigger has already been installed */
64
+ private readonly installedTriggers;
65
+ constructor(options: PostgresAdapterOptions);
66
+ /** Connect the LISTEN client and install the shared trigger function. */
67
+ connect(): Promise<void>;
68
+ /** Disconnect the LISTEN client and optionally clean up triggers. */
69
+ disconnect(): Promise<void>;
70
+ /**
71
+ * Register a callback for changes on `table`.
72
+ * Installs a PostgreSQL trigger on first registration for this table.
73
+ *
74
+ * @returns An unsubscribe function.
75
+ */
76
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
77
+ private ensureTriggerInstalled;
78
+ private handleNotification;
79
+ }
80
+
81
+ export { type NotifyPayload, PostgresAdapter, type PostgresAdapterOptions };
@@ -0,0 +1,81 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from '../types-tPDla8AE.js';
2
+
3
+ /**
4
+ * Configuration options for PostgresAdapter.
5
+ */
6
+ interface PostgresAdapterOptions {
7
+ /**
8
+ * PostgreSQL connection string.
9
+ * e.g. 'postgresql://user:password@localhost:5432/dbname'
10
+ */
11
+ connectionString: string;
12
+ /**
13
+ * Schema to install trigger functions in.
14
+ * Defaults to 'public'.
15
+ */
16
+ schema?: string;
17
+ /**
18
+ * Name prefix for generated trigger functions and triggers.
19
+ * Defaults to 'reactive_api'.
20
+ */
21
+ triggerPrefix?: string;
22
+ }
23
+ /**
24
+ * The raw JSON payload delivered by the PostgreSQL NOTIFY.
25
+ * Must stay in sync with the trigger function SQL in trigger-sql.ts.
26
+ */
27
+ interface NotifyPayload {
28
+ table: string;
29
+ operation: 'INSERT' | 'UPDATE' | 'DELETE';
30
+ new_row: Record<string, unknown> | null;
31
+ old_row: Record<string, unknown> | null;
32
+ }
33
+
34
+ /**
35
+ * PostgreSQL database adapter for RouteFlow.
36
+ *
37
+ * Uses a dedicated `pg.Client` (not a pool) for LISTEN so the connection
38
+ * stays open and notifications are never missed. A separate pool/client
39
+ * can still be used by the application for regular queries.
40
+ *
41
+ * Change detection mechanism:
42
+ * - On `onChange(table)`: installs a per-table AFTER INSERT/UPDATE/DELETE trigger
43
+ * that calls `pg_notify` with a JSON payload.
44
+ * - A single LISTEN connection subscribes to one shared channel for all tables.
45
+ * - Payloads > 7900 bytes are sent without row data (`_truncated: true`);
46
+ * subscribers receive a ChangeEvent with `newRow: null` / `oldRow: null`.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const adapter = new PostgresAdapter({
51
+ * connectionString: process.env.DATABASE_URL,
52
+ * })
53
+ * await adapter.connect()
54
+ * ```
55
+ */
56
+ declare class PostgresAdapter implements DatabaseAdapter {
57
+ private listenClient;
58
+ private readonly schema;
59
+ private readonly prefix;
60
+ private readonly connectionString;
61
+ /** table → Set<callback> */
62
+ private readonly listeners;
63
+ /** Tables for which a trigger has already been installed */
64
+ private readonly installedTriggers;
65
+ constructor(options: PostgresAdapterOptions);
66
+ /** Connect the LISTEN client and install the shared trigger function. */
67
+ connect(): Promise<void>;
68
+ /** Disconnect the LISTEN client and optionally clean up triggers. */
69
+ disconnect(): Promise<void>;
70
+ /**
71
+ * Register a callback for changes on `table`.
72
+ * Installs a PostgreSQL trigger on first registration for this table.
73
+ *
74
+ * @returns An unsubscribe function.
75
+ */
76
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
77
+ private ensureTriggerInstalled;
78
+ private handleNotification;
79
+ }
80
+
81
+ export { type NotifyPayload, PostgresAdapter, type PostgresAdapterOptions };