@stepflowjs/trigger-postgres 0.0.1

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.
@@ -0,0 +1,67 @@
1
+ import { Trigger, TriggerHandler } from '@stepflowjs/core';
2
+
3
+ interface PostgresChangeTriggerConfig {
4
+ /** PostgreSQL connection string */
5
+ connectionString: string;
6
+ /** LISTEN/NOTIFY channel name */
7
+ channel: string;
8
+ /** Optional table to watch for changes */
9
+ table?: string;
10
+ /** Operations to watch (if table is specified) */
11
+ operations?: ("INSERT" | "UPDATE" | "DELETE")[];
12
+ }
13
+ interface PostgresChangePayload {
14
+ operation?: "INSERT" | "UPDATE" | "DELETE";
15
+ table?: string;
16
+ old?: Record<string, unknown>;
17
+ new?: Record<string, unknown>;
18
+ [key: string]: unknown;
19
+ }
20
+ /**
21
+ * PostgreSQL LISTEN/NOTIFY trigger for Stepflow workflows
22
+ *
23
+ * Listens to PostgreSQL NOTIFY events on a specified channel. Optionally creates
24
+ * database triggers to automatically notify on table changes (INSERT, UPDATE, DELETE).
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const trigger = new PostgresChangeTrigger({
29
+ * connectionString: process.env.DATABASE_URL,
30
+ * channel: 'order_changes',
31
+ * table: 'orders',
32
+ * operations: ['INSERT', 'UPDATE'],
33
+ * });
34
+ *
35
+ * await trigger.start(async (event) => {
36
+ * await stepflow.trigger('process-order', event.data);
37
+ * });
38
+ * ```
39
+ */
40
+ declare class PostgresChangeTrigger implements Trigger<PostgresChangeTriggerConfig> {
41
+ readonly config: PostgresChangeTriggerConfig;
42
+ readonly type = "postgres-change";
43
+ private client?;
44
+ private pool;
45
+ private handler?;
46
+ constructor(config: PostgresChangeTriggerConfig);
47
+ /**
48
+ * Start the trigger with a handler function
49
+ * @param handler Function to call when PostgreSQL notifications are received
50
+ */
51
+ start(handler: TriggerHandler): Promise<void>;
52
+ /**
53
+ * Set up database trigger function and trigger on the specified table
54
+ * @private
55
+ */
56
+ private setupTableTrigger;
57
+ /**
58
+ * Stop the trigger and clean up resources
59
+ */
60
+ stop(): Promise<void>;
61
+ /**
62
+ * Health check - verifies database connection
63
+ */
64
+ healthCheck(): Promise<boolean>;
65
+ }
66
+
67
+ export { type PostgresChangePayload, PostgresChangeTrigger, type PostgresChangeTriggerConfig };
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ // src/index.ts
2
+ import pg from "pg";
3
+ var { Pool } = pg;
4
+ var PostgresChangeTrigger = class {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.pool = new Pool({ connectionString: config.connectionString });
8
+ }
9
+ type = "postgres-change";
10
+ client;
11
+ pool;
12
+ handler;
13
+ /**
14
+ * Start the trigger with a handler function
15
+ * @param handler Function to call when PostgreSQL notifications are received
16
+ */
17
+ async start(handler) {
18
+ this.handler = handler;
19
+ this.client = await this.pool.connect();
20
+ if (this.config.table) {
21
+ await this.setupTableTrigger();
22
+ }
23
+ await this.client.query(`LISTEN ${this.config.channel}`);
24
+ this.client.on("notification", async (msg) => {
25
+ if (msg.channel === this.config.channel && this.handler) {
26
+ try {
27
+ let payload = {};
28
+ if (msg.payload) {
29
+ try {
30
+ payload = JSON.parse(msg.payload);
31
+ } catch {
32
+ payload = { message: msg.payload };
33
+ }
34
+ }
35
+ const event = {
36
+ id: crypto.randomUUID(),
37
+ type: this.type,
38
+ source: this.config.channel,
39
+ data: payload,
40
+ metadata: {
41
+ channel: msg.channel,
42
+ table: this.config.table,
43
+ operation: payload.operation
44
+ },
45
+ timestamp: /* @__PURE__ */ new Date()
46
+ };
47
+ await this.handler(event);
48
+ } catch (error) {
49
+ console.error("Error handling PostgreSQL notification:", error);
50
+ }
51
+ }
52
+ });
53
+ this.client.on("error", (error) => {
54
+ console.error("PostgreSQL client error:", error);
55
+ });
56
+ }
57
+ /**
58
+ * Set up database trigger function and trigger on the specified table
59
+ * @private
60
+ */
61
+ async setupTableTrigger() {
62
+ if (!this.client || !this.config.table) {
63
+ return;
64
+ }
65
+ const {
66
+ table,
67
+ channel,
68
+ operations = ["INSERT", "UPDATE", "DELETE"]
69
+ } = this.config;
70
+ const functionName = `${table}_notify_${channel}`;
71
+ const triggerName = `${table}_trigger_${channel}`;
72
+ try {
73
+ await this.client.query(`
74
+ CREATE OR REPLACE FUNCTION ${functionName}()
75
+ RETURNS trigger AS $$
76
+ DECLARE
77
+ payload json;
78
+ BEGIN
79
+ -- Build JSON payload with operation and row data
80
+ IF (TG_OP = 'DELETE') THEN
81
+ payload = json_build_object(
82
+ 'operation', TG_OP,
83
+ 'table', TG_TABLE_NAME,
84
+ 'old', row_to_json(OLD)
85
+ );
86
+ ELSIF (TG_OP = 'UPDATE') THEN
87
+ payload = json_build_object(
88
+ 'operation', TG_OP,
89
+ 'table', TG_TABLE_NAME,
90
+ 'old', row_to_json(OLD),
91
+ 'new', row_to_json(NEW)
92
+ );
93
+ ELSIF (TG_OP = 'INSERT') THEN
94
+ payload = json_build_object(
95
+ 'operation', TG_OP,
96
+ 'table', TG_TABLE_NAME,
97
+ 'new', row_to_json(NEW)
98
+ );
99
+ END IF;
100
+
101
+ -- Send notification
102
+ PERFORM pg_notify('${channel}', payload::text);
103
+
104
+ -- Return appropriate row
105
+ IF (TG_OP = 'DELETE') THEN
106
+ RETURN OLD;
107
+ ELSE
108
+ RETURN NEW;
109
+ END IF;
110
+ END;
111
+ $$ LANGUAGE plpgsql;
112
+ `);
113
+ await this.client.query(`
114
+ DROP TRIGGER IF EXISTS ${triggerName} ON ${table};
115
+ `);
116
+ const triggerOps = operations.join(" OR ");
117
+ await this.client.query(`
118
+ CREATE TRIGGER ${triggerName}
119
+ AFTER ${triggerOps} ON ${table}
120
+ FOR EACH ROW
121
+ EXECUTE FUNCTION ${functionName}();
122
+ `);
123
+ } catch (error) {
124
+ console.error("Error setting up table trigger:", error);
125
+ throw new Error(
126
+ `Failed to set up trigger on table "${table}": ${error.message}`
127
+ );
128
+ }
129
+ }
130
+ /**
131
+ * Stop the trigger and clean up resources
132
+ */
133
+ async stop() {
134
+ if (this.client) {
135
+ try {
136
+ await this.client.query(`UNLISTEN ${this.config.channel}`);
137
+ } catch (error) {
138
+ console.error("Error during UNLISTEN:", error);
139
+ } finally {
140
+ this.client.release();
141
+ this.client = void 0;
142
+ }
143
+ }
144
+ try {
145
+ await this.pool.end();
146
+ } catch (error) {
147
+ console.error("Error closing pool:", error);
148
+ }
149
+ this.handler = void 0;
150
+ }
151
+ /**
152
+ * Health check - verifies database connection
153
+ */
154
+ async healthCheck() {
155
+ try {
156
+ await this.pool.query("SELECT 1");
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ };
163
+ export {
164
+ PostgresChangeTrigger
165
+ };
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Trigger, TriggerHandler, TriggerEvent } from \"@stepflowjs/core\";\nimport pg from \"pg\";\n\nconst { Pool } = pg;\ntype PoolClient = pg.PoolClient;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface PostgresChangeTriggerConfig {\n /** PostgreSQL connection string */\n connectionString: string;\n /** LISTEN/NOTIFY channel name */\n channel: string;\n /** Optional table to watch for changes */\n table?: string;\n /** Operations to watch (if table is specified) */\n operations?: (\"INSERT\" | \"UPDATE\" | \"DELETE\")[];\n}\n\nexport interface PostgresChangePayload {\n operation?: \"INSERT\" | \"UPDATE\" | \"DELETE\";\n table?: string;\n old?: Record<string, unknown>;\n new?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\n// ============================================================================\n// PostgresChangeTrigger Implementation\n// ============================================================================\n\n/**\n * PostgreSQL LISTEN/NOTIFY trigger for Stepflow workflows\n *\n * Listens to PostgreSQL NOTIFY events on a specified channel. Optionally creates\n * database triggers to automatically notify on table changes (INSERT, UPDATE, DELETE).\n *\n * @example\n * ```typescript\n * const trigger = new PostgresChangeTrigger({\n * connectionString: process.env.DATABASE_URL,\n * channel: 'order_changes',\n * table: 'orders',\n * operations: ['INSERT', 'UPDATE'],\n * });\n *\n * await trigger.start(async (event) => {\n * await stepflow.trigger('process-order', event.data);\n * });\n * ```\n */\nexport class PostgresChangeTrigger implements Trigger<PostgresChangeTriggerConfig> {\n readonly type = \"postgres-change\";\n private client?: PoolClient;\n private pool: pg.Pool;\n private handler?: TriggerHandler;\n\n constructor(readonly config: PostgresChangeTriggerConfig) {\n this.pool = new Pool({ connectionString: config.connectionString });\n }\n\n /**\n * Start the trigger with a handler function\n * @param handler Function to call when PostgreSQL notifications are received\n */\n async start(handler: TriggerHandler): Promise<void> {\n this.handler = handler;\n\n // Connect a dedicated client for LISTEN\n this.client = await this.pool.connect();\n\n // Set up table trigger if table is specified\n if (this.config.table) {\n await this.setupTableTrigger();\n }\n\n // Start listening on the channel\n await this.client.query(`LISTEN ${this.config.channel}`);\n\n // Handle notifications\n this.client.on(\"notification\", async (msg) => {\n if (msg.channel === this.config.channel && this.handler) {\n try {\n // Parse payload (may be JSON or plain text)\n let payload: PostgresChangePayload = {};\n if (msg.payload) {\n try {\n payload = JSON.parse(msg.payload);\n } catch {\n // If not JSON, treat as plain text\n payload = { message: msg.payload };\n }\n }\n\n // Create trigger event\n const event: TriggerEvent = {\n id: crypto.randomUUID(),\n type: this.type,\n source: this.config.channel,\n data: payload,\n metadata: {\n channel: msg.channel,\n table: this.config.table,\n operation: payload.operation,\n },\n timestamp: new Date(),\n };\n\n await this.handler(event);\n } catch (error) {\n console.error(\"Error handling PostgreSQL notification:\", error);\n }\n }\n });\n\n // Handle connection errors\n this.client.on(\"error\", (error) => {\n console.error(\"PostgreSQL client error:\", error);\n });\n }\n\n /**\n * Set up database trigger function and trigger on the specified table\n * @private\n */\n private async setupTableTrigger(): Promise<void> {\n if (!this.client || !this.config.table) {\n return;\n }\n\n const {\n table,\n channel,\n operations = [\"INSERT\", \"UPDATE\", \"DELETE\"],\n } = this.config;\n const functionName = `${table}_notify_${channel}`;\n const triggerName = `${table}_trigger_${channel}`;\n\n try {\n // Create or replace the trigger function\n await this.client.query(`\n CREATE OR REPLACE FUNCTION ${functionName}()\n RETURNS trigger AS $$\n DECLARE\n payload json;\n BEGIN\n -- Build JSON payload with operation and row data\n IF (TG_OP = 'DELETE') THEN\n payload = json_build_object(\n 'operation', TG_OP,\n 'table', TG_TABLE_NAME,\n 'old', row_to_json(OLD)\n );\n ELSIF (TG_OP = 'UPDATE') THEN\n payload = json_build_object(\n 'operation', TG_OP,\n 'table', TG_TABLE_NAME,\n 'old', row_to_json(OLD),\n 'new', row_to_json(NEW)\n );\n ELSIF (TG_OP = 'INSERT') THEN\n payload = json_build_object(\n 'operation', TG_OP,\n 'table', TG_TABLE_NAME,\n 'new', row_to_json(NEW)\n );\n END IF;\n\n -- Send notification\n PERFORM pg_notify('${channel}', payload::text);\n\n -- Return appropriate row\n IF (TG_OP = 'DELETE') THEN\n RETURN OLD;\n ELSE\n RETURN NEW;\n END IF;\n END;\n $$ LANGUAGE plpgsql;\n `);\n\n // Drop existing trigger if it exists\n await this.client.query(`\n DROP TRIGGER IF EXISTS ${triggerName} ON ${table};\n `);\n\n // Create trigger for specified operations\n const triggerOps = operations.join(\" OR \");\n await this.client.query(`\n CREATE TRIGGER ${triggerName}\n AFTER ${triggerOps} ON ${table}\n FOR EACH ROW\n EXECUTE FUNCTION ${functionName}();\n `);\n } catch (error) {\n console.error(\"Error setting up table trigger:\", error);\n throw new Error(\n `Failed to set up trigger on table \"${table}\": ${(error as Error).message}`,\n );\n }\n }\n\n /**\n * Stop the trigger and clean up resources\n */\n async stop(): Promise<void> {\n if (this.client) {\n try {\n // Stop listening\n await this.client.query(`UNLISTEN ${this.config.channel}`);\n } catch (error) {\n console.error(\"Error during UNLISTEN:\", error);\n } finally {\n // Release the client back to the pool\n this.client.release();\n this.client = undefined;\n }\n }\n\n // Close the pool\n try {\n await this.pool.end();\n } catch (error) {\n console.error(\"Error closing pool:\", error);\n }\n\n this.handler = undefined;\n }\n\n /**\n * Health check - verifies database connection\n */\n async healthCheck(): Promise<boolean> {\n try {\n await this.pool.query(\"SELECT 1\");\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@stepflowjs/trigger-postgres",
3
+ "version": "0.0.1",
4
+ "description": "PostgreSQL LISTEN/NOTIFY trigger for Stepflow",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "pg": "^8.13.0",
20
+ "@stepflowjs/core": "0.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/pg": "^8.11.0",
24
+ "tsup": "^8.5.1",
25
+ "vitest": "^4.0.17"
26
+ },
27
+ "peerDependencies": {
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "license": "MIT",
31
+ "author": "Stepflow Contributors",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://stepflow-production.up.railway.app",
35
+ "directory": "packages/triggers/postgres"
36
+ },
37
+ "homepage": "https://stepflow-production.up.railway.app",
38
+ "bugs": {
39
+ "url": "https://stepflow-production.up.railway.app"
40
+ },
41
+ "keywords": [
42
+ "stepflow",
43
+ "trigger",
44
+ "postgres",
45
+ "postgresql",
46
+ "listen",
47
+ "notify",
48
+ "workflow",
49
+ "orchestration"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest",
59
+ "clean": "rm -rf dist"
60
+ }
61
+ }