@pgflow/core 0.0.0-pgflow-installer-45a8ec76-20251211182851 → 0.0.0-pgflow-installer-53178542-20251211201625

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 (51) hide show
  1. package/dist/CHANGELOG.md +2 -2
  2. package/dist/package.json +1 -5
  3. package/package.json +2 -6
  4. package/dist/migrations/_generated/m_20250429164909.d.ts +0 -3
  5. package/dist/migrations/_generated/m_20250429164909.d.ts.map +0 -1
  6. package/dist/migrations/_generated/m_20250429164909.js +0 -584
  7. package/dist/migrations/_generated/m_20250517072017.d.ts +0 -3
  8. package/dist/migrations/_generated/m_20250517072017.d.ts.map +0 -1
  9. package/dist/migrations/_generated/m_20250517072017.js +0 -106
  10. package/dist/migrations/_generated/m_20250609105135.d.ts +0 -3
  11. package/dist/migrations/_generated/m_20250609105135.d.ts.map +0 -1
  12. package/dist/migrations/_generated/m_20250609105135.js +0 -376
  13. package/dist/migrations/_generated/m_20250610180554.d.ts +0 -3
  14. package/dist/migrations/_generated/m_20250610180554.d.ts.map +0 -1
  15. package/dist/migrations/_generated/m_20250610180554.js +0 -132
  16. package/dist/migrations/_generated/m_20250614124241.d.ts +0 -3
  17. package/dist/migrations/_generated/m_20250614124241.d.ts.map +0 -1
  18. package/dist/migrations/_generated/m_20250614124241.js +0 -506
  19. package/dist/migrations/_generated/m_20250619195327.d.ts +0 -3
  20. package/dist/migrations/_generated/m_20250619195327.d.ts.map +0 -1
  21. package/dist/migrations/_generated/m_20250619195327.js +0 -190
  22. package/dist/migrations/_generated/m_20250627090700.d.ts +0 -3
  23. package/dist/migrations/_generated/m_20250627090700.d.ts.map +0 -1
  24. package/dist/migrations/_generated/m_20250627090700.js +0 -11
  25. package/dist/migrations/_generated/m_20250707210212.d.ts +0 -3
  26. package/dist/migrations/_generated/m_20250707210212.d.ts.map +0 -1
  27. package/dist/migrations/_generated/m_20250707210212.js +0 -108
  28. package/dist/migrations/_generated/m_20250719205006.d.ts +0 -3
  29. package/dist/migrations/_generated/m_20250719205006.d.ts.map +0 -1
  30. package/dist/migrations/_generated/m_20250719205006.js +0 -7
  31. package/dist/migrations/_generated/m_20251006073122.d.ts +0 -3
  32. package/dist/migrations/_generated/m_20251006073122.d.ts.map +0 -1
  33. package/dist/migrations/_generated/m_20251006073122.js +0 -1249
  34. package/dist/migrations/_generated/m_20251103222045.d.ts +0 -3
  35. package/dist/migrations/_generated/m_20251103222045.d.ts.map +0 -1
  36. package/dist/migrations/_generated/m_20251103222045.js +0 -627
  37. package/dist/migrations/_generated/m_20251104080523.d.ts +0 -3
  38. package/dist/migrations/_generated/m_20251104080523.d.ts.map +0 -1
  39. package/dist/migrations/_generated/m_20251104080523.js +0 -98
  40. package/dist/migrations/_generated/m_20251130000000.d.ts +0 -3
  41. package/dist/migrations/_generated/m_20251130000000.d.ts.map +0 -1
  42. package/dist/migrations/_generated/m_20251130000000.js +0 -273
  43. package/dist/migrations/_generated/m_20251209074533.d.ts +0 -3
  44. package/dist/migrations/_generated/m_20251209074533.d.ts.map +0 -1
  45. package/dist/migrations/_generated/m_20251209074533.js +0 -278
  46. package/dist/migrations/index.d.ts +0 -11
  47. package/dist/migrations/index.d.ts.map +0 -1
  48. package/dist/migrations/index.js +0 -39
  49. package/dist/migrations/types.d.ts +0 -12
  50. package/dist/migrations/types.d.ts.map +0 -1
  51. package/dist/migrations/types.js +0 -1
@@ -1,98 +0,0 @@
1
- export const migration = {
2
- timestamp: '20251104080523',
3
- filename: '20251104080523_pgflow_upgrade_pgmq_1_5_1.sql',
4
- content: `-- Migration tested 2025-11-02:
5
- -- Successfully verified that this migration fails on pgmq 1.4.4 (Supabase CLI < 2.50.3)
6
- -- with clear error message guiding users to upgrade pgmq to 1.5.0+
7
- --
8
- -- Compatibility check: Ensure pgmq.message_record has headers column (pgmq 1.5.0+)
9
- DO $$
10
- DECLARE
11
- has_headers BOOLEAN;
12
- BEGIN
13
- SELECT EXISTS (
14
- SELECT 1
15
- FROM pg_type t
16
- JOIN pg_namespace n ON t.typnamespace = n.oid
17
- JOIN pg_attribute a ON a.attrelid = t.typrelid
18
- WHERE n.nspname = 'pgmq'
19
- AND t.typname = 'message_record'
20
- AND a.attname = 'headers'
21
- AND a.attnum > 0
22
- AND NOT a.attisdropped
23
- ) INTO has_headers;
24
-
25
- IF NOT has_headers THEN
26
- RAISE EXCEPTION E'INCOMPATIBLE PGMQ VERSION DETECTED\\n\\n'
27
- 'This migration is part of pgflow 0.8.0+, which requires pgmq 1.5.0 or higher.\\n'
28
- 'The pgmq.message_record type is missing the "headers" column, which indicates you are running pgmq < 1.5.0.\\n\\n'
29
- 'pgflow 0.8.0+ is NOT compatible with pgmq versions below 1.5.0.\\n\\n'
30
- 'Action required:\\n'
31
- ' - If using Supabase: Ensure you are running a recent version that includes pgmq 1.5.0+\\n'
32
- ' - If self-hosting: Upgrade pgmq to version 1.5.0 or higher before running this migration\\n\\n'
33
- 'Migration aborted to prevent runtime failures.';
34
- END IF;
35
- END $$;
36
-
37
- -- Modify "set_vt_batch" function
38
- -- Must drop first because we're changing the return type from SETOF to TABLE
39
- DROP FUNCTION IF EXISTS "pgflow"."set_vt_batch"(text, bigint[], integer[]);
40
- CREATE FUNCTION "pgflow"."set_vt_batch" (
41
- "queue_name" text,
42
- "msg_ids" bigint[],
43
- "vt_offsets" integer[]
44
- )
45
- RETURNS TABLE(
46
- msg_id bigint,
47
- read_ct integer,
48
- enqueued_at timestamp with time zone,
49
- vt timestamp with time zone,
50
- message jsonb,
51
- headers jsonb
52
- )
53
- LANGUAGE plpgsql AS $$
54
- DECLARE
55
- qtable TEXT := pgmq.format_table_name(queue_name, 'q');
56
- sql TEXT;
57
- BEGIN
58
- /* ---------- safety checks ---------------------------------------------------- */
59
- IF msg_ids IS NULL OR vt_offsets IS NULL OR array_length(msg_ids, 1) = 0 THEN
60
- RETURN; -- nothing to do, return empty set
61
- END IF;
62
-
63
- IF array_length(msg_ids, 1) IS DISTINCT FROM array_length(vt_offsets, 1) THEN
64
- RAISE EXCEPTION
65
- 'msg_ids length (%) must equal vt_offsets length (%)',
66
- array_length(msg_ids, 1), array_length(vt_offsets, 1);
67
- END IF;
68
-
69
- /* ---------- dynamic statement ------------------------------------------------ */
70
- /* One UPDATE joins with the unnested arrays */
71
- sql := format(
72
- $FMT$
73
- WITH input (msg_id, vt_offset) AS (
74
- SELECT unnest($1)::bigint
75
- , unnest($2)::int
76
- )
77
- UPDATE pgmq.%I q
78
- SET vt = clock_timestamp() + make_interval(secs => input.vt_offset),
79
- read_ct = read_ct -- no change, but keeps RETURNING list aligned
80
- FROM input
81
- WHERE q.msg_id = input.msg_id
82
- RETURNING q.msg_id,
83
- q.read_ct,
84
- q.enqueued_at,
85
- q.vt,
86
- q.message,
87
- q.headers
88
- $FMT$,
89
- qtable
90
- );
91
-
92
- RETURN QUERY EXECUTE sql USING msg_ids, vt_offsets;
93
- END;
94
- $$;
95
- -- Drop "read_with_poll" function
96
- DROP FUNCTION "pgflow"."read_with_poll";
97
- `,
98
- };
@@ -1,3 +0,0 @@
1
- import type { Migration } from '../types.js';
2
- export declare const migration: Migration;
3
- //# sourceMappingURL=m_20251130000000.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"m_20251130000000.d.ts","sourceRoot":"","sources":["../../../src/migrations/_generated/m_20251130000000.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,SAAS,EAAE,SAgRvB,CAAC"}
@@ -1,273 +0,0 @@
1
- export const migration = {
2
- timestamp: '20251130000000',
3
- filename: '20251130000000_pgflow_auto_compilation.sql',
4
- content: `-- Modify "create_flow" function
5
- CREATE OR REPLACE FUNCTION "pgflow"."create_flow" ("flow_slug" text, "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer) RETURNS "pgflow"."flows" LANGUAGE sql SET "search_path" = '' AS $$
6
- WITH
7
- defaults AS (
8
- SELECT 3 AS def_max_attempts, 5 AS def_base_delay, 60 AS def_timeout
9
- ),
10
- flow_upsert AS (
11
- INSERT INTO pgflow.flows (flow_slug, opt_max_attempts, opt_base_delay, opt_timeout)
12
- SELECT
13
- flow_slug,
14
- COALESCE(max_attempts, defaults.def_max_attempts),
15
- COALESCE(base_delay, defaults.def_base_delay),
16
- COALESCE(timeout, defaults.def_timeout)
17
- FROM defaults
18
- ON CONFLICT (flow_slug) DO UPDATE
19
- SET flow_slug = pgflow.flows.flow_slug -- Dummy update
20
- RETURNING *
21
- ),
22
- ensure_queue AS (
23
- SELECT pgmq.create(flow_slug)
24
- WHERE NOT EXISTS (
25
- SELECT 1 FROM pgmq.list_queues() WHERE queue_name = flow_slug
26
- )
27
- )
28
- SELECT f.*
29
- FROM flow_upsert f
30
- LEFT JOIN (SELECT 1 FROM ensure_queue) _dummy ON true; -- Left join ensures flow is returned
31
- $$;
32
- -- Create "_compare_flow_shapes" function
33
- CREATE FUNCTION "pgflow"."_compare_flow_shapes" ("p_local" jsonb, "p_db" jsonb) RETURNS text[] LANGUAGE plpgsql STABLE SET "search_path" = '' AS $BODY$
34
- DECLARE
35
- v_differences text[] := '{}';
36
- v_local_steps jsonb;
37
- v_db_steps jsonb;
38
- v_local_count int;
39
- v_db_count int;
40
- v_max_count int;
41
- v_idx int;
42
- v_local_step jsonb;
43
- v_db_step jsonb;
44
- v_local_deps text;
45
- v_db_deps text;
46
- BEGIN
47
- v_local_steps := p_local->'steps';
48
- v_db_steps := p_db->'steps';
49
- v_local_count := jsonb_array_length(COALESCE(v_local_steps, '[]'::jsonb));
50
- v_db_count := jsonb_array_length(COALESCE(v_db_steps, '[]'::jsonb));
51
-
52
- -- Compare step counts
53
- IF v_local_count != v_db_count THEN
54
- v_differences := array_append(
55
- v_differences,
56
- format('Step count differs: %s vs %s', v_local_count, v_db_count)
57
- );
58
- END IF;
59
-
60
- -- Compare steps by index
61
- v_max_count := GREATEST(v_local_count, v_db_count);
62
-
63
- FOR v_idx IN 0..(v_max_count - 1) LOOP
64
- v_local_step := v_local_steps->v_idx;
65
- v_db_step := v_db_steps->v_idx;
66
-
67
- IF v_local_step IS NULL THEN
68
- v_differences := array_append(
69
- v_differences,
70
- format(
71
- $$Step at index %s: missing in first shape (second has '%s')$$,
72
- v_idx,
73
- v_db_step->>'slug'
74
- )
75
- );
76
- ELSIF v_db_step IS NULL THEN
77
- v_differences := array_append(
78
- v_differences,
79
- format(
80
- $$Step at index %s: missing in second shape (first has '%s')$$,
81
- v_idx,
82
- v_local_step->>'slug'
83
- )
84
- );
85
- ELSE
86
- -- Compare slug
87
- IF v_local_step->>'slug' != v_db_step->>'slug' THEN
88
- v_differences := array_append(
89
- v_differences,
90
- format(
91
- $$Step at index %s: slug differs '%s' vs '%s'$$,
92
- v_idx,
93
- v_local_step->>'slug',
94
- v_db_step->>'slug'
95
- )
96
- );
97
- END IF;
98
-
99
- -- Compare step type
100
- IF v_local_step->>'stepType' != v_db_step->>'stepType' THEN
101
- v_differences := array_append(
102
- v_differences,
103
- format(
104
- $$Step at index %s: type differs '%s' vs '%s'$$,
105
- v_idx,
106
- v_local_step->>'stepType',
107
- v_db_step->>'stepType'
108
- )
109
- );
110
- END IF;
111
-
112
- -- Compare dependencies (convert arrays to comma-separated strings)
113
- SELECT string_agg(dep, ', ' ORDER BY dep)
114
- INTO v_local_deps
115
- FROM jsonb_array_elements_text(COALESCE(v_local_step->'dependencies', '[]'::jsonb)) AS dep;
116
-
117
- SELECT string_agg(dep, ', ' ORDER BY dep)
118
- INTO v_db_deps
119
- FROM jsonb_array_elements_text(COALESCE(v_db_step->'dependencies', '[]'::jsonb)) AS dep;
120
-
121
- IF COALESCE(v_local_deps, '') != COALESCE(v_db_deps, '') THEN
122
- v_differences := array_append(
123
- v_differences,
124
- format(
125
- $$Step at index %s: dependencies differ [%s] vs [%s]$$,
126
- v_idx,
127
- COALESCE(v_local_deps, ''),
128
- COALESCE(v_db_deps, '')
129
- )
130
- );
131
- END IF;
132
- END IF;
133
- END LOOP;
134
-
135
- RETURN v_differences;
136
- END;
137
- $BODY$;
138
- -- Create "_create_flow_from_shape" function
139
- CREATE FUNCTION "pgflow"."_create_flow_from_shape" ("p_flow_slug" text, "p_shape" jsonb) RETURNS void LANGUAGE plpgsql SET "search_path" = '' AS $$
140
- DECLARE
141
- v_step jsonb;
142
- v_deps text[];
143
- v_flow_options jsonb;
144
- v_step_options jsonb;
145
- BEGIN
146
- -- Extract flow-level options (may be null)
147
- v_flow_options := p_shape->'options';
148
-
149
- -- Create the flow with options (NULL = use default)
150
- PERFORM pgflow.create_flow(
151
- p_flow_slug,
152
- (v_flow_options->>'maxAttempts')::int,
153
- (v_flow_options->>'baseDelay')::int,
154
- (v_flow_options->>'timeout')::int
155
- );
156
-
157
- -- Iterate over steps in order and add each one
158
- FOR v_step IN SELECT * FROM jsonb_array_elements(p_shape->'steps')
159
- LOOP
160
- -- Convert dependencies jsonb array to text array
161
- SELECT COALESCE(array_agg(dep), '{}')
162
- INTO v_deps
163
- FROM jsonb_array_elements_text(COALESCE(v_step->'dependencies', '[]'::jsonb)) AS dep;
164
-
165
- -- Extract step options (may be null)
166
- v_step_options := v_step->'options';
167
-
168
- -- Add the step with options (NULL = use default/inherit)
169
- PERFORM pgflow.add_step(
170
- flow_slug => p_flow_slug,
171
- step_slug => v_step->>'slug',
172
- deps_slugs => v_deps,
173
- max_attempts => (v_step_options->>'maxAttempts')::int,
174
- base_delay => (v_step_options->>'baseDelay')::int,
175
- timeout => (v_step_options->>'timeout')::int,
176
- start_delay => (v_step_options->>'startDelay')::int,
177
- step_type => v_step->>'stepType'
178
- );
179
- END LOOP;
180
- END;
181
- $$;
182
- -- Create "_get_flow_shape" function
183
- CREATE FUNCTION "pgflow"."_get_flow_shape" ("p_flow_slug" text) RETURNS jsonb LANGUAGE sql STABLE SET "search_path" = '' AS $$
184
- SELECT jsonb_build_object(
185
- 'steps',
186
- COALESCE(
187
- jsonb_agg(
188
- jsonb_build_object(
189
- 'slug', step.step_slug,
190
- 'stepType', step.step_type,
191
- 'dependencies', COALESCE(
192
- (
193
- SELECT jsonb_agg(dep.dep_slug ORDER BY dep.dep_slug)
194
- FROM pgflow.deps AS dep
195
- WHERE dep.flow_slug = step.flow_slug
196
- AND dep.step_slug = step.step_slug
197
- ),
198
- '[]'::jsonb
199
- )
200
- )
201
- ORDER BY step.step_index
202
- ),
203
- '[]'::jsonb
204
- )
205
- )
206
- FROM pgflow.steps AS step
207
- WHERE step.flow_slug = p_flow_slug;
208
- $$;
209
- -- Create "delete_flow_and_data" function
210
- CREATE FUNCTION "pgflow"."delete_flow_and_data" ("p_flow_slug" text) RETURNS void LANGUAGE plpgsql SET "search_path" = '' AS $$
211
- BEGIN
212
- -- Drop queue and archive table (pgmq)
213
- PERFORM pgmq.drop_queue(p_flow_slug);
214
-
215
- -- Delete all associated data in the correct order (respecting FK constraints)
216
- DELETE FROM pgflow.step_tasks AS task WHERE task.flow_slug = p_flow_slug;
217
- DELETE FROM pgflow.step_states AS state WHERE state.flow_slug = p_flow_slug;
218
- DELETE FROM pgflow.runs AS run WHERE run.flow_slug = p_flow_slug;
219
- DELETE FROM pgflow.deps AS dep WHERE dep.flow_slug = p_flow_slug;
220
- DELETE FROM pgflow.steps AS step WHERE step.flow_slug = p_flow_slug;
221
- DELETE FROM pgflow.flows AS flow WHERE flow.flow_slug = p_flow_slug;
222
- END;
223
- $$;
224
- -- Create "ensure_flow_compiled" function
225
- CREATE FUNCTION "pgflow"."ensure_flow_compiled" ("p_flow_slug" text, "p_shape" jsonb, "p_mode" text DEFAULT 'production') RETURNS jsonb LANGUAGE plpgsql SET "search_path" = '' AS $$
226
- DECLARE
227
- v_lock_key int;
228
- v_flow_exists boolean;
229
- v_db_shape jsonb;
230
- v_differences text[];
231
- BEGIN
232
- -- Generate lock key from flow_slug (deterministic hash)
233
- v_lock_key := hashtext(p_flow_slug);
234
-
235
- -- Acquire transaction-level advisory lock
236
- -- Serializes concurrent compilation attempts for same flow
237
- PERFORM pg_advisory_xact_lock(1, v_lock_key);
238
-
239
- -- 1. Check if flow exists
240
- SELECT EXISTS(SELECT 1 FROM pgflow.flows AS flow WHERE flow.flow_slug = p_flow_slug)
241
- INTO v_flow_exists;
242
-
243
- -- 2. If flow missing: compile (both modes)
244
- IF NOT v_flow_exists THEN
245
- PERFORM pgflow._create_flow_from_shape(p_flow_slug, p_shape);
246
- RETURN jsonb_build_object('status', 'compiled', 'differences', '[]'::jsonb);
247
- END IF;
248
-
249
- -- 3. Get current shape from DB
250
- v_db_shape := pgflow._get_flow_shape(p_flow_slug);
251
-
252
- -- 4. Compare shapes
253
- v_differences := pgflow._compare_flow_shapes(p_shape, v_db_shape);
254
-
255
- -- 5. If shapes match: return verified
256
- IF array_length(v_differences, 1) IS NULL THEN
257
- RETURN jsonb_build_object('status', 'verified', 'differences', '[]'::jsonb);
258
- END IF;
259
-
260
- -- 6. Shapes differ - handle by mode
261
- IF p_mode = 'development' THEN
262
- -- Recompile in dev mode: full deletion + fresh compile
263
- PERFORM pgflow.delete_flow_and_data(p_flow_slug);
264
- PERFORM pgflow._create_flow_from_shape(p_flow_slug, p_shape);
265
- RETURN jsonb_build_object('status', 'recompiled', 'differences', to_jsonb(v_differences));
266
- ELSE
267
- -- Fail in production mode
268
- RETURN jsonb_build_object('status', 'mismatch', 'differences', to_jsonb(v_differences));
269
- END IF;
270
- END;
271
- $$;
272
- `,
273
- };
@@ -1,3 +0,0 @@
1
- import type { Migration } from '../types.js';
2
- export declare const migration: Migration;
3
- //# sourceMappingURL=m_20251209074533.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"m_20251209074533.d.ts","sourceRoot":"","sources":["../../../src/migrations/_generated/m_20251209074533.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,SAAS,EAAE,SAqRvB,CAAC"}
@@ -1,278 +0,0 @@
1
- export const migration = {
2
- timestamp: '20251209074533',
3
- filename: '20251209074533_pgflow_worker_management.sql',
4
- content: `-- Create extension "pg_net" if not exists
5
- CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "public";
6
- -- Create extension "pg_cron" if not exists
7
- CREATE EXTENSION IF NOT EXISTS "pg_cron";
8
- -- Modify "workers" table
9
- ALTER TABLE "pgflow"."workers" ADD COLUMN "stopped_at" timestamptz NULL;
10
- -- Create "worker_functions" table
11
- CREATE TABLE "pgflow"."worker_functions" (
12
- "function_name" text NOT NULL,
13
- "enabled" boolean NOT NULL DEFAULT true,
14
- "debounce" interval NOT NULL DEFAULT '00:00:06'::interval,
15
- "last_invoked_at" timestamptz NULL,
16
- "created_at" timestamptz NOT NULL DEFAULT now(),
17
- "updated_at" timestamptz NOT NULL DEFAULT now(),
18
- PRIMARY KEY ("function_name"),
19
- CONSTRAINT "worker_functions_debounce_check" CHECK (debounce >= '00:00:01'::interval)
20
- );
21
- -- Set comment to table: "worker_functions"
22
- COMMENT ON TABLE "pgflow"."worker_functions" IS 'Registry of edge functions that run pgflow workers, used by ensure_workers() cron';
23
- -- Set comment to column: "function_name" on table: "worker_functions"
24
- COMMENT ON COLUMN "pgflow"."worker_functions"."function_name" IS 'Name of the Supabase Edge Function';
25
- -- Set comment to column: "enabled" on table: "worker_functions"
26
- COMMENT ON COLUMN "pgflow"."worker_functions"."enabled" IS 'Whether ensure_workers() should ping this function';
27
- -- Set comment to column: "debounce" on table: "worker_functions"
28
- COMMENT ON COLUMN "pgflow"."worker_functions"."debounce" IS 'Minimum interval between invocation attempts for this function';
29
- -- Set comment to column: "last_invoked_at" on table: "worker_functions"
30
- COMMENT ON COLUMN "pgflow"."worker_functions"."last_invoked_at" IS 'When ensure_workers() last pinged this function (used for debouncing)';
31
- -- Create "cleanup_ensure_workers_logs" function
32
- CREATE FUNCTION "pgflow"."cleanup_ensure_workers_logs" ("retention_hours" integer DEFAULT 24) RETURNS TABLE ("cron_deleted" bigint) LANGUAGE sql SECURITY DEFINER SET "search_path" = pgflow, cron, pg_temp AS $$
33
- with deleted as (
34
- delete from cron.job_run_details
35
- where job_run_details.end_time < now() - (cleanup_ensure_workers_logs.retention_hours || ' hours')::interval
36
- returning 1
37
- )
38
- select count(*)::bigint as cron_deleted from deleted
39
- $$;
40
- -- Set comment to function: "cleanup_ensure_workers_logs"
41
- COMMENT ON FUNCTION "pgflow"."cleanup_ensure_workers_logs" IS 'Cleans up old cron job run details to prevent table growth.
42
- Default retention is 24 hours. HTTP response logs (net._http_response) are
43
- automatically cleaned by pg_net with a 6-hour TTL, so they are not cleaned here.
44
- This function follows the standard pg_cron maintenance pattern recommended by
45
- AWS RDS, Neon, and Supabase documentation.';
46
- -- Create "is_local" function
47
- CREATE FUNCTION "pgflow"."is_local" () RETURNS boolean LANGUAGE sql STABLE PARALLEL SAFE SET "search_path" = '' AS $$
48
- select coalesce(
49
- current_setting('app.settings.jwt_secret', true)
50
- = 'super-secret-jwt-token-with-at-least-32-characters-long',
51
- false
52
- )
53
- $$;
54
- -- Create "ensure_flow_compiled" function
55
- CREATE FUNCTION "pgflow"."ensure_flow_compiled" ("flow_slug" text, "shape" jsonb) RETURNS jsonb LANGUAGE plpgsql SET "search_path" = '' AS $$
56
- DECLARE
57
- v_lock_key int;
58
- v_flow_exists boolean;
59
- v_db_shape jsonb;
60
- v_differences text[];
61
- v_is_local boolean;
62
- BEGIN
63
- -- Generate lock key from flow_slug (deterministic hash)
64
- v_lock_key := hashtext(ensure_flow_compiled.flow_slug);
65
-
66
- -- Acquire transaction-level advisory lock
67
- -- Serializes concurrent compilation attempts for same flow
68
- PERFORM pg_advisory_xact_lock(1, v_lock_key);
69
-
70
- -- 1. Check if flow exists
71
- SELECT EXISTS(SELECT 1 FROM pgflow.flows AS flow WHERE flow.flow_slug = ensure_flow_compiled.flow_slug)
72
- INTO v_flow_exists;
73
-
74
- -- 2. If flow missing: compile (both environments)
75
- IF NOT v_flow_exists THEN
76
- PERFORM pgflow._create_flow_from_shape(ensure_flow_compiled.flow_slug, ensure_flow_compiled.shape);
77
- RETURN jsonb_build_object('status', 'compiled', 'differences', '[]'::jsonb);
78
- END IF;
79
-
80
- -- 3. Get current shape from DB
81
- v_db_shape := pgflow._get_flow_shape(ensure_flow_compiled.flow_slug);
82
-
83
- -- 4. Compare shapes
84
- v_differences := pgflow._compare_flow_shapes(ensure_flow_compiled.shape, v_db_shape);
85
-
86
- -- 5. If shapes match: return verified
87
- IF array_length(v_differences, 1) IS NULL THEN
88
- RETURN jsonb_build_object('status', 'verified', 'differences', '[]'::jsonb);
89
- END IF;
90
-
91
- -- 6. Shapes differ - auto-detect environment via is_local()
92
- v_is_local := pgflow.is_local();
93
-
94
- IF v_is_local THEN
95
- -- Recompile in local/dev: full deletion + fresh compile
96
- PERFORM pgflow.delete_flow_and_data(ensure_flow_compiled.flow_slug);
97
- PERFORM pgflow._create_flow_from_shape(ensure_flow_compiled.flow_slug, ensure_flow_compiled.shape);
98
- RETURN jsonb_build_object('status', 'recompiled', 'differences', to_jsonb(v_differences));
99
- ELSE
100
- -- Fail in production
101
- RETURN jsonb_build_object('status', 'mismatch', 'differences', to_jsonb(v_differences));
102
- END IF;
103
- END;
104
- $$;
105
- -- Create "ensure_workers" function
106
- CREATE FUNCTION "pgflow"."ensure_workers" () RETURNS TABLE ("function_name" text, "invoked" boolean, "request_id" bigint) LANGUAGE sql AS $$
107
- with
108
- -- Detect environment
109
- env as (
110
- select pgflow.is_local() as is_local
111
- ),
112
-
113
- -- Get credentials: Local mode uses hardcoded URL, production uses vault secrets
114
- -- Empty strings are treated as NULL using nullif()
115
- credentials as (
116
- select
117
- case
118
- when (select is_local from env) then null
119
- else nullif((select decrypted_secret from vault.decrypted_secrets where name = 'supabase_service_role_key'), '')
120
- end as service_role_key,
121
- case
122
- when (select is_local from env) then 'http://kong:8000/functions/v1'
123
- else (select 'https://' || nullif(decrypted_secret, '') || '.supabase.co/functions/v1' from vault.decrypted_secrets where name = 'supabase_project_id')
124
- end as base_url
125
- ),
126
-
127
- -- Find functions that pass the debounce check
128
- debounce_passed as (
129
- select wf.function_name, wf.debounce
130
- from pgflow.worker_functions as wf
131
- where wf.enabled = true
132
- and (
133
- wf.last_invoked_at is null
134
- or wf.last_invoked_at < now() - wf.debounce
135
- )
136
- ),
137
-
138
- -- Find functions that have at least one alive worker
139
- functions_with_alive_workers as (
140
- select distinct w.function_name
141
- from pgflow.workers as w
142
- inner join debounce_passed as dp on w.function_name = dp.function_name
143
- where w.stopped_at is null
144
- and w.deprecated_at is null
145
- and w.last_heartbeat_at > now() - dp.debounce
146
- ),
147
-
148
- -- Determine which functions should be invoked
149
- -- Local mode: all enabled functions (bypass debounce AND alive workers check)
150
- -- Production mode: only functions that pass debounce AND have no alive workers
151
- functions_to_invoke as (
152
- select wf.function_name
153
- from pgflow.worker_functions as wf
154
- where wf.enabled = true
155
- and (
156
- pgflow.is_local() = true -- Local: all enabled functions
157
- or (
158
- -- Production: debounce + no alive workers
159
- wf.function_name in (select dp.function_name from debounce_passed as dp)
160
- and wf.function_name not in (select faw.function_name from functions_with_alive_workers as faw)
161
- )
162
- )
163
- ),
164
-
165
- -- Make HTTP requests and capture request_ids
166
- http_requests as (
167
- select
168
- fti.function_name,
169
- net.http_post(
170
- url => c.base_url || '/' || fti.function_name,
171
- headers => case
172
- when e.is_local then '{}'::jsonb
173
- else jsonb_build_object(
174
- 'Content-Type', 'application/json',
175
- 'Authorization', 'Bearer ' || c.service_role_key
176
- )
177
- end,
178
- body => '{}'::jsonb
179
- ) as request_id
180
- from functions_to_invoke as fti
181
- cross join credentials as c
182
- cross join env as e
183
- where c.base_url is not null
184
- and (e.is_local or c.service_role_key is not null)
185
- ),
186
-
187
- -- Update last_invoked_at for invoked functions
188
- updated as (
189
- update pgflow.worker_functions as wf
190
- set last_invoked_at = clock_timestamp()
191
- from http_requests as hr
192
- where wf.function_name = hr.function_name
193
- returning wf.function_name
194
- )
195
-
196
- select u.function_name, true as invoked, hr.request_id
197
- from updated as u
198
- inner join http_requests as hr on u.function_name = hr.function_name
199
- $$;
200
- -- Set comment to function: "ensure_workers"
201
- COMMENT ON FUNCTION "pgflow"."ensure_workers" IS 'Ensures worker functions are running by pinging them via HTTP when needed.
202
- In local mode: pings ALL enabled functions (ignores debounce AND alive workers check).
203
- In production mode: only pings functions that pass debounce AND have no alive workers.
204
- Debounce: skips functions pinged within their debounce interval (production only).
205
- Credentials: Uses Vault secrets (supabase_service_role_key, supabase_project_id) or local fallbacks.
206
- URL is built from project_id: https://{project_id}.supabase.co/functions/v1
207
- Returns request_id from pg_net for each HTTP request made.';
208
- -- Create "mark_worker_stopped" function
209
- CREATE FUNCTION "pgflow"."mark_worker_stopped" ("worker_id" uuid) RETURNS void LANGUAGE sql AS $$
210
- update pgflow.workers
211
- set stopped_at = clock_timestamp()
212
- where workers.worker_id = mark_worker_stopped.worker_id;
213
- $$;
214
- -- Set comment to function: "mark_worker_stopped"
215
- COMMENT ON FUNCTION "pgflow"."mark_worker_stopped" IS 'Marks a worker as stopped for graceful shutdown. Called by workers on beforeunload.';
216
- -- Create "setup_ensure_workers_cron" function
217
- CREATE FUNCTION "pgflow"."setup_ensure_workers_cron" ("cron_interval" text DEFAULT '1 second') RETURNS text LANGUAGE plpgsql SECURITY DEFINER SET "search_path" = pgflow, cron, pg_temp AS $$
218
- declare
219
- ensure_workers_job_id bigint;
220
- cleanup_job_id bigint;
221
- begin
222
- -- Remove existing jobs if they exist (ignore errors if not found)
223
- begin
224
- perform cron.unschedule('pgflow_ensure_workers');
225
- exception when others then
226
- -- Job doesn't exist, continue
227
- end;
228
-
229
- begin
230
- perform cron.unschedule('pgflow_cleanup_logs');
231
- exception when others then
232
- -- Job doesn't exist, continue
233
- end;
234
-
235
- -- Schedule ensure_workers job with the specified interval
236
- ensure_workers_job_id := cron.schedule(
237
- job_name => 'pgflow_ensure_workers',
238
- schedule => setup_ensure_workers_cron.cron_interval,
239
- command => 'select pgflow.ensure_workers()'
240
- );
241
-
242
- -- Schedule cleanup job to run hourly
243
- cleanup_job_id := cron.schedule(
244
- job_name => 'pgflow_cleanup_logs',
245
- schedule => '0 * * * *',
246
- command => 'select pgflow.cleanup_ensure_workers_logs()'
247
- );
248
-
249
- return format(
250
- 'Scheduled pgflow_ensure_workers (every %s, job_id=%s) and pgflow_cleanup_logs (hourly, job_id=%s)',
251
- setup_ensure_workers_cron.cron_interval,
252
- ensure_workers_job_id,
253
- cleanup_job_id
254
- );
255
- end;
256
- $$;
257
- -- Set comment to function: "setup_ensure_workers_cron"
258
- COMMENT ON FUNCTION "pgflow"."setup_ensure_workers_cron" IS 'Sets up cron jobs for worker management.
259
- Schedules pgflow_ensure_workers at the specified cron_interval (default: 1 second) to keep workers running.
260
- Schedules pgflow_cleanup_logs hourly to clean up old cron job logs.
261
- Replaces existing jobs if they exist (idempotent).
262
- Returns a confirmation message with job IDs.';
263
- -- Create "track_worker_function" function
264
- CREATE FUNCTION "pgflow"."track_worker_function" ("function_name" text) RETURNS void LANGUAGE sql AS $$
265
- insert into pgflow.worker_functions (function_name, updated_at)
266
- values (track_worker_function.function_name, clock_timestamp())
267
- on conflict (function_name)
268
- do update set
269
- updated_at = clock_timestamp();
270
- $$;
271
- -- Set comment to function: "track_worker_function"
272
- COMMENT ON FUNCTION "pgflow"."track_worker_function" IS 'Registers an edge function for monitoring. Called by workers on startup.';
273
- -- Drop "ensure_flow_compiled" function
274
- DROP FUNCTION "pgflow"."ensure_flow_compiled" (text, jsonb, text);
275
- -- Auto-install ensure_workers cron job (1 second interval)
276
- SELECT pgflow.setup_ensure_workers_cron('1 second');
277
- `,
278
- };
@@ -1,11 +0,0 @@
1
- /**
2
- * Auto-generated migration exports
3
- *
4
- * This file is regenerated by scripts/generate-migrations.ts
5
- * Do not edit manually.
6
- */
7
- import type { Migration } from './types.js';
8
- export declare const migrations: Migration[];
9
- export declare function getMigrations(): Migration[];
10
- export type { Migration };
11
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAgB5C,eAAO,MAAM,UAAU,EAAE,SAAS,EAejC,CAAC;AAEF,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}