@supacloud/lite 0.37.1 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/cli.js +40 -39
- package/dist/index.js +40 -39
- package/dist/runtime/db/emulated.d.ts +1 -1
- package/dist/runtime/db/emulated.d.ts.map +1 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.37.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.37.1...supacloud-lite-v0.37.2) (2026-11-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Elegance & Refactoring
|
|
7
|
+
|
|
8
|
+
* **commands:** name command and artifact row columns ([#2418](https://github.com/vibeunion/supacloud/issues/2418)) ([7da4967](https://github.com/vibeunion/supacloud/commit/7da496706a152a3c3cbe7529d46ab4ac6c6b2bf0))
|
|
9
|
+
* **workflows:** name workflow row columns ([#2417](https://github.com/vibeunion/supacloud/issues/2417)) ([5e3e9c2](https://github.com/vibeunion/supacloud/commit/5e3e9c20141e0e47ac939888b18cce7f938fabc1))
|
|
10
|
+
|
|
3
11
|
## [0.37.1](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.37.0...supacloud-lite-v0.37.1) (2026-11-02)
|
|
4
12
|
|
|
5
13
|
|
package/dist/cli.js
CHANGED
|
@@ -12010,7 +12010,7 @@ import { dirname as dirname9, join as join11, resolve as resolve8 } from "path";
|
|
|
12010
12010
|
// package.json
|
|
12011
12011
|
var package_default = {
|
|
12012
12012
|
name: "@supacloud/lite",
|
|
12013
|
-
version: "0.37.
|
|
12013
|
+
version: "0.37.2",
|
|
12014
12014
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
12015
12015
|
type: "module",
|
|
12016
12016
|
license: "Apache-2.0",
|
|
@@ -12070,7 +12070,7 @@ var package_default = {
|
|
|
12070
12070
|
"@electric-sql/pglite-pgmq": "0.0.8",
|
|
12071
12071
|
"@electric-sql/pglite-pgvector": "0.0.11",
|
|
12072
12072
|
"@electric-sql/pglite-postgis": "0.2.10",
|
|
12073
|
-
"@supacloud/db": "0.20.
|
|
12073
|
+
"@supacloud/db": "0.20.5",
|
|
12074
12074
|
"exact-mirror": "1.2.6",
|
|
12075
12075
|
"fast-xml-parser": "5.11.2",
|
|
12076
12076
|
tar: "^7.5.22",
|
|
@@ -12079,8 +12079,8 @@ var package_default = {
|
|
|
12079
12079
|
devDependencies: {
|
|
12080
12080
|
"@supacloud/app": "0.36.0",
|
|
12081
12081
|
"@supacloud/contracts": "0.15.2",
|
|
12082
|
-
"@supacloud/compiler": "0.
|
|
12083
|
-
"@supacloud/elysia": "0.38.
|
|
12082
|
+
"@supacloud/compiler": "0.55.0",
|
|
12083
|
+
"@supacloud/elysia": "0.38.1",
|
|
12084
12084
|
"@supabase/supabase-js": "^2.118.0",
|
|
12085
12085
|
"@types/bun": "^1.4.4",
|
|
12086
12086
|
effect: "4.0.5",
|
|
@@ -12089,10 +12089,10 @@ var package_default = {
|
|
|
12089
12089
|
typescript: "^7.0.2"
|
|
12090
12090
|
},
|
|
12091
12091
|
overrides: {
|
|
12092
|
-
"@supacloud/db": "0.20.
|
|
12092
|
+
"@supacloud/db": "0.20.5",
|
|
12093
12093
|
"@supacloud/app": "0.36.0",
|
|
12094
|
-
"@supacloud/compiler": "0.
|
|
12095
|
-
"@supacloud/elysia": "0.38.
|
|
12094
|
+
"@supacloud/compiler": "0.55.0",
|
|
12095
|
+
"@supacloud/elysia": "0.38.1",
|
|
12096
12096
|
"@supacloud/contracts": "0.15.2",
|
|
12097
12097
|
"@supacloud/commands": "0.11.2",
|
|
12098
12098
|
"@supacloud/delivery": "0.24.0",
|
|
@@ -19014,7 +19014,7 @@ BEGIN
|
|
|
19014
19014
|
id, run_id, step_key, input, max_attempts, queue_message_id
|
|
19015
19015
|
) VALUES (
|
|
19016
19016
|
step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
|
|
19017
|
-
) RETURNING
|
|
19017
|
+
) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;
|
|
19018
19018
|
|
|
19019
19019
|
RETURN created_step;
|
|
19020
19020
|
END;
|
|
@@ -19054,11 +19054,11 @@ BEGIN
|
|
|
19054
19054
|
END IF;
|
|
19055
19055
|
|
|
19056
19056
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
19057
|
-
SELECT
|
|
19057
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;
|
|
19058
19058
|
IF FOUND THEN
|
|
19059
|
-
SELECT
|
|
19060
|
-
FROM supacloud_workflows.steps
|
|
19061
|
-
WHERE run_id = p_run_id
|
|
19059
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step
|
|
19060
|
+
FROM supacloud_workflows.steps workflow_step
|
|
19061
|
+
WHERE workflow_step.run_id = p_run_id
|
|
19062
19062
|
ORDER BY created_at, id
|
|
19063
19063
|
LIMIT 1;
|
|
19064
19064
|
IF NOT FOUND THEN
|
|
@@ -19146,11 +19146,11 @@ BEGIN
|
|
|
19146
19146
|
IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
|
|
19147
19147
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
|
|
19148
19148
|
END IF;
|
|
19149
|
-
SELECT
|
|
19150
|
-
FROM supacloud_workflows.steps
|
|
19151
|
-
WHERE id::text = lower(message_step_id)
|
|
19152
|
-
AND run_id = candidate_run_id
|
|
19153
|
-
AND queue_message_id = queued_message.msg_id
|
|
19149
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step
|
|
19150
|
+
FROM supacloud_workflows.steps workflow_step
|
|
19151
|
+
WHERE workflow_step.id::text = lower(message_step_id)
|
|
19152
|
+
AND workflow_step.run_id = candidate_run_id
|
|
19153
|
+
AND workflow_step.queue_message_id = queued_message.msg_id
|
|
19154
19154
|
FOR UPDATE;
|
|
19155
19155
|
IF NOT FOUND THEN
|
|
19156
19156
|
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
@@ -19161,9 +19161,9 @@ BEGIN
|
|
|
19161
19161
|
);
|
|
19162
19162
|
END IF;
|
|
19163
19163
|
|
|
19164
|
-
SELECT
|
|
19165
|
-
FROM supacloud_workflows.runs
|
|
19166
|
-
WHERE id = claimed_step.run_id
|
|
19164
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run
|
|
19165
|
+
FROM supacloud_workflows.runs workflow_run
|
|
19166
|
+
WHERE workflow_run.id = claimed_step.run_id
|
|
19167
19167
|
FOR UPDATE;
|
|
19168
19168
|
IF claimed_run.status NOT IN ('queued', 'running')
|
|
19169
19169
|
OR claimed_step.status NOT IN ('queued', 'running') THEN
|
|
@@ -19250,9 +19250,9 @@ BEGIN
|
|
|
19250
19250
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
19251
19251
|
END IF;
|
|
19252
19252
|
PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
|
|
19253
|
-
SELECT
|
|
19254
|
-
FROM supacloud_workflows.steps
|
|
19255
|
-
WHERE id = p_step_id
|
|
19253
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
|
|
19254
|
+
FROM supacloud_workflows.steps workflow_step
|
|
19255
|
+
WHERE workflow_step.id = p_step_id
|
|
19256
19256
|
FOR UPDATE;
|
|
19257
19257
|
IF NOT FOUND THEN
|
|
19258
19258
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
@@ -19314,9 +19314,9 @@ BEGIN
|
|
|
19314
19314
|
);
|
|
19315
19315
|
|
|
19316
19316
|
IF current_step.status = 'completed' THEN
|
|
19317
|
-
SELECT
|
|
19318
|
-
FROM supacloud_workflows.steps
|
|
19319
|
-
WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
|
|
19317
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step
|
|
19318
|
+
FROM supacloud_workflows.steps workflow_step
|
|
19319
|
+
WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;
|
|
19320
19320
|
IF NOT FOUND
|
|
19321
19321
|
OR current_step.output IS DISTINCT FROM p_output
|
|
19322
19322
|
OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
|
|
@@ -19372,9 +19372,9 @@ BEGIN
|
|
|
19372
19372
|
current_step := supacloud_workflows.lock_step_attempt(
|
|
19373
19373
|
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
19374
19374
|
);
|
|
19375
|
-
SELECT
|
|
19376
|
-
FROM supacloud_workflows.runs
|
|
19377
|
-
WHERE id = current_step.run_id
|
|
19375
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
|
|
19376
|
+
FROM supacloud_workflows.runs workflow_run
|
|
19377
|
+
WHERE workflow_run.id = current_step.run_id
|
|
19378
19378
|
FOR UPDATE;
|
|
19379
19379
|
|
|
19380
19380
|
IF current_step.status = 'completed' THEN
|
|
@@ -19550,9 +19550,9 @@ BEGIN
|
|
|
19550
19550
|
current_step := supacloud_workflows.lock_step_attempt(
|
|
19551
19551
|
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
19552
19552
|
);
|
|
19553
|
-
SELECT
|
|
19554
|
-
FROM supacloud_workflows.runs
|
|
19555
|
-
WHERE id = current_step.run_id
|
|
19553
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
|
|
19554
|
+
FROM supacloud_workflows.runs workflow_run
|
|
19555
|
+
WHERE workflow_run.id = current_step.run_id
|
|
19556
19556
|
FOR UPDATE;
|
|
19557
19557
|
|
|
19558
19558
|
IF current_step.status = 'failed' THEN
|
|
@@ -19606,7 +19606,7 @@ BEGIN
|
|
|
19606
19606
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
19607
19607
|
END IF;
|
|
19608
19608
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
19609
|
-
SELECT
|
|
19609
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;
|
|
19610
19610
|
IF NOT FOUND THEN
|
|
19611
19611
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
19612
19612
|
END IF;
|
|
@@ -19619,9 +19619,9 @@ BEGIN
|
|
|
19619
19619
|
IF locked_run.status NOT IN ('queued', 'running') THEN
|
|
19620
19620
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
19621
19621
|
END IF;
|
|
19622
|
-
SELECT
|
|
19623
|
-
FROM supacloud_workflows.steps
|
|
19624
|
-
WHERE run_id = p_run_id AND status IN ('queued', 'running')
|
|
19622
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
|
|
19623
|
+
FROM supacloud_workflows.steps workflow_step
|
|
19624
|
+
WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')
|
|
19625
19625
|
FOR UPDATE;
|
|
19626
19626
|
IF FOUND THEN
|
|
19627
19627
|
PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
|
|
@@ -20016,7 +20016,7 @@ BEGIN
|
|
|
20016
20016
|
|
|
20017
20017
|
fingerprint := 'md5:' || md5(payload::text);
|
|
20018
20018
|
PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
|
|
20019
|
-
SELECT
|
|
20019
|
+
SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;
|
|
20020
20020
|
IF FOUND THEN
|
|
20021
20021
|
IF existing.command_type <> normalized_command_type
|
|
20022
20022
|
OR existing.target_type <> normalized_target_type
|
|
@@ -20296,7 +20296,7 @@ BEGIN
|
|
|
20296
20296
|
END IF;
|
|
20297
20297
|
|
|
20298
20298
|
PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
|
|
20299
|
-
SELECT
|
|
20299
|
+
SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;
|
|
20300
20300
|
IF FOUND THEN
|
|
20301
20301
|
IF existing.storage_object_id <> object_row.id
|
|
20302
20302
|
OR existing.artifact_type <> normalized_type
|
|
@@ -33055,7 +33055,8 @@ ${COMMAND_PERSISTENCE_SQL}
|
|
|
33055
33055
|
DO $backfill$
|
|
33056
33056
|
DECLARE r record;
|
|
33057
33057
|
BEGIN
|
|
33058
|
-
FOR r IN SELECT
|
|
33058
|
+
FOR r IN SELECT dispatch_key, tenant_id, actor_id, command, operation_key
|
|
33059
|
+
FROM supacloud_commands.execution_receipts
|
|
33059
33060
|
WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
|
|
33060
33061
|
AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
|
|
33061
33062
|
LOOP
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
|
|
|
88
88
|
// package.json
|
|
89
89
|
var package_default = {
|
|
90
90
|
name: "@supacloud/lite",
|
|
91
|
-
version: "0.37.
|
|
91
|
+
version: "0.37.2",
|
|
92
92
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
93
93
|
type: "module",
|
|
94
94
|
license: "Apache-2.0",
|
|
@@ -148,7 +148,7 @@ var package_default = {
|
|
|
148
148
|
"@electric-sql/pglite-pgmq": "0.0.8",
|
|
149
149
|
"@electric-sql/pglite-pgvector": "0.0.11",
|
|
150
150
|
"@electric-sql/pglite-postgis": "0.2.10",
|
|
151
|
-
"@supacloud/db": "0.20.
|
|
151
|
+
"@supacloud/db": "0.20.5",
|
|
152
152
|
"exact-mirror": "1.2.6",
|
|
153
153
|
"fast-xml-parser": "5.11.2",
|
|
154
154
|
tar: "^7.5.22",
|
|
@@ -157,8 +157,8 @@ var package_default = {
|
|
|
157
157
|
devDependencies: {
|
|
158
158
|
"@supacloud/app": "0.36.0",
|
|
159
159
|
"@supacloud/contracts": "0.15.2",
|
|
160
|
-
"@supacloud/compiler": "0.
|
|
161
|
-
"@supacloud/elysia": "0.38.
|
|
160
|
+
"@supacloud/compiler": "0.55.0",
|
|
161
|
+
"@supacloud/elysia": "0.38.1",
|
|
162
162
|
"@supabase/supabase-js": "^2.118.0",
|
|
163
163
|
"@types/bun": "^1.4.4",
|
|
164
164
|
effect: "4.0.5",
|
|
@@ -167,10 +167,10 @@ var package_default = {
|
|
|
167
167
|
typescript: "^7.0.2"
|
|
168
168
|
},
|
|
169
169
|
overrides: {
|
|
170
|
-
"@supacloud/db": "0.20.
|
|
170
|
+
"@supacloud/db": "0.20.5",
|
|
171
171
|
"@supacloud/app": "0.36.0",
|
|
172
|
-
"@supacloud/compiler": "0.
|
|
173
|
-
"@supacloud/elysia": "0.38.
|
|
172
|
+
"@supacloud/compiler": "0.55.0",
|
|
173
|
+
"@supacloud/elysia": "0.38.1",
|
|
174
174
|
"@supacloud/contracts": "0.15.2",
|
|
175
175
|
"@supacloud/commands": "0.11.2",
|
|
176
176
|
"@supacloud/delivery": "0.24.0",
|
|
@@ -7018,7 +7018,7 @@ BEGIN
|
|
|
7018
7018
|
id, run_id, step_key, input, max_attempts, queue_message_id
|
|
7019
7019
|
) VALUES (
|
|
7020
7020
|
step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
|
|
7021
|
-
) RETURNING
|
|
7021
|
+
) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;
|
|
7022
7022
|
|
|
7023
7023
|
RETURN created_step;
|
|
7024
7024
|
END;
|
|
@@ -7058,11 +7058,11 @@ BEGIN
|
|
|
7058
7058
|
END IF;
|
|
7059
7059
|
|
|
7060
7060
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
7061
|
-
SELECT
|
|
7061
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;
|
|
7062
7062
|
IF FOUND THEN
|
|
7063
|
-
SELECT
|
|
7064
|
-
FROM supacloud_workflows.steps
|
|
7065
|
-
WHERE run_id = p_run_id
|
|
7063
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step
|
|
7064
|
+
FROM supacloud_workflows.steps workflow_step
|
|
7065
|
+
WHERE workflow_step.run_id = p_run_id
|
|
7066
7066
|
ORDER BY created_at, id
|
|
7067
7067
|
LIMIT 1;
|
|
7068
7068
|
IF NOT FOUND THEN
|
|
@@ -7150,11 +7150,11 @@ BEGIN
|
|
|
7150
7150
|
IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
|
|
7151
7151
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
|
|
7152
7152
|
END IF;
|
|
7153
|
-
SELECT
|
|
7154
|
-
FROM supacloud_workflows.steps
|
|
7155
|
-
WHERE id::text = lower(message_step_id)
|
|
7156
|
-
AND run_id = candidate_run_id
|
|
7157
|
-
AND queue_message_id = queued_message.msg_id
|
|
7153
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step
|
|
7154
|
+
FROM supacloud_workflows.steps workflow_step
|
|
7155
|
+
WHERE workflow_step.id::text = lower(message_step_id)
|
|
7156
|
+
AND workflow_step.run_id = candidate_run_id
|
|
7157
|
+
AND workflow_step.queue_message_id = queued_message.msg_id
|
|
7158
7158
|
FOR UPDATE;
|
|
7159
7159
|
IF NOT FOUND THEN
|
|
7160
7160
|
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
@@ -7165,9 +7165,9 @@ BEGIN
|
|
|
7165
7165
|
);
|
|
7166
7166
|
END IF;
|
|
7167
7167
|
|
|
7168
|
-
SELECT
|
|
7169
|
-
FROM supacloud_workflows.runs
|
|
7170
|
-
WHERE id = claimed_step.run_id
|
|
7168
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run
|
|
7169
|
+
FROM supacloud_workflows.runs workflow_run
|
|
7170
|
+
WHERE workflow_run.id = claimed_step.run_id
|
|
7171
7171
|
FOR UPDATE;
|
|
7172
7172
|
IF claimed_run.status NOT IN ('queued', 'running')
|
|
7173
7173
|
OR claimed_step.status NOT IN ('queued', 'running') THEN
|
|
@@ -7254,9 +7254,9 @@ BEGIN
|
|
|
7254
7254
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
7255
7255
|
END IF;
|
|
7256
7256
|
PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
|
|
7257
|
-
SELECT
|
|
7258
|
-
FROM supacloud_workflows.steps
|
|
7259
|
-
WHERE id = p_step_id
|
|
7257
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
|
|
7258
|
+
FROM supacloud_workflows.steps workflow_step
|
|
7259
|
+
WHERE workflow_step.id = p_step_id
|
|
7260
7260
|
FOR UPDATE;
|
|
7261
7261
|
IF NOT FOUND THEN
|
|
7262
7262
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
@@ -7318,9 +7318,9 @@ BEGIN
|
|
|
7318
7318
|
);
|
|
7319
7319
|
|
|
7320
7320
|
IF current_step.status = 'completed' THEN
|
|
7321
|
-
SELECT
|
|
7322
|
-
FROM supacloud_workflows.steps
|
|
7323
|
-
WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
|
|
7321
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step
|
|
7322
|
+
FROM supacloud_workflows.steps workflow_step
|
|
7323
|
+
WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;
|
|
7324
7324
|
IF NOT FOUND
|
|
7325
7325
|
OR current_step.output IS DISTINCT FROM p_output
|
|
7326
7326
|
OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
|
|
@@ -7376,9 +7376,9 @@ BEGIN
|
|
|
7376
7376
|
current_step := supacloud_workflows.lock_step_attempt(
|
|
7377
7377
|
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
7378
7378
|
);
|
|
7379
|
-
SELECT
|
|
7380
|
-
FROM supacloud_workflows.runs
|
|
7381
|
-
WHERE id = current_step.run_id
|
|
7379
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
|
|
7380
|
+
FROM supacloud_workflows.runs workflow_run
|
|
7381
|
+
WHERE workflow_run.id = current_step.run_id
|
|
7382
7382
|
FOR UPDATE;
|
|
7383
7383
|
|
|
7384
7384
|
IF current_step.status = 'completed' THEN
|
|
@@ -7554,9 +7554,9 @@ BEGIN
|
|
|
7554
7554
|
current_step := supacloud_workflows.lock_step_attempt(
|
|
7555
7555
|
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
7556
7556
|
);
|
|
7557
|
-
SELECT
|
|
7558
|
-
FROM supacloud_workflows.runs
|
|
7559
|
-
WHERE id = current_step.run_id
|
|
7557
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
|
|
7558
|
+
FROM supacloud_workflows.runs workflow_run
|
|
7559
|
+
WHERE workflow_run.id = current_step.run_id
|
|
7560
7560
|
FOR UPDATE;
|
|
7561
7561
|
|
|
7562
7562
|
IF current_step.status = 'failed' THEN
|
|
@@ -7610,7 +7610,7 @@ BEGIN
|
|
|
7610
7610
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
7611
7611
|
END IF;
|
|
7612
7612
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
7613
|
-
SELECT
|
|
7613
|
+
SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;
|
|
7614
7614
|
IF NOT FOUND THEN
|
|
7615
7615
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
7616
7616
|
END IF;
|
|
@@ -7623,9 +7623,9 @@ BEGIN
|
|
|
7623
7623
|
IF locked_run.status NOT IN ('queued', 'running') THEN
|
|
7624
7624
|
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
7625
7625
|
END IF;
|
|
7626
|
-
SELECT
|
|
7627
|
-
FROM supacloud_workflows.steps
|
|
7628
|
-
WHERE run_id = p_run_id AND status IN ('queued', 'running')
|
|
7626
|
+
SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
|
|
7627
|
+
FROM supacloud_workflows.steps workflow_step
|
|
7628
|
+
WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')
|
|
7629
7629
|
FOR UPDATE;
|
|
7630
7630
|
IF FOUND THEN
|
|
7631
7631
|
PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
|
|
@@ -8020,7 +8020,7 @@ BEGIN
|
|
|
8020
8020
|
|
|
8021
8021
|
fingerprint := 'md5:' || md5(payload::text);
|
|
8022
8022
|
PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
|
|
8023
|
-
SELECT
|
|
8023
|
+
SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;
|
|
8024
8024
|
IF FOUND THEN
|
|
8025
8025
|
IF existing.command_type <> normalized_command_type
|
|
8026
8026
|
OR existing.target_type <> normalized_target_type
|
|
@@ -8300,7 +8300,7 @@ BEGIN
|
|
|
8300
8300
|
END IF;
|
|
8301
8301
|
|
|
8302
8302
|
PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
|
|
8303
|
-
SELECT
|
|
8303
|
+
SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;
|
|
8304
8304
|
IF FOUND THEN
|
|
8305
8305
|
IF existing.storage_object_id <> object_row.id
|
|
8306
8306
|
OR existing.artifact_type <> normalized_type
|
|
@@ -21694,7 +21694,8 @@ ${COMMAND_PERSISTENCE_SQL}
|
|
|
21694
21694
|
DO $backfill$
|
|
21695
21695
|
DECLARE r record;
|
|
21696
21696
|
BEGIN
|
|
21697
|
-
FOR r IN SELECT
|
|
21697
|
+
FOR r IN SELECT dispatch_key, tenant_id, actor_id, command, operation_key
|
|
21698
|
+
FROM supacloud_commands.execution_receipts
|
|
21698
21699
|
WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
|
|
21699
21700
|
AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
|
|
21700
21701
|
LOOP
|
|
@@ -19,7 +19,7 @@ export declare const PGMQ_CORE_SQL = "\ncreate schema if not exists pgmq;\nrevok
|
|
|
19
19
|
* extension supplies the pgmq schema; this SQL only owns SupaCloud wrappers. */
|
|
20
20
|
export declare const PGMQ_PUBLIC_SQL = "\n-- supacloud:sql-module:pgmq-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\n\nCREATE SCHEMA IF NOT EXISTS pgmq_public;\nGRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;\nCREATE SCHEMA IF NOT EXISTS supacloud_queue;\nCREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (\n queue_name text NOT NULL,\n job_key text NOT NULL,\n msg_id bigint NOT NULL,\n message jsonb NOT NULL,\n sleep_seconds integer NOT NULL,\n created_at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (queue_name, job_key)\n);\n-- Upgrade early key-only installations without inventing an input identity.\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;\nREVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)\nRETURNS text\nLANGUAGE plpgsql\nIMMUTABLE\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue_name text := lower(btrim(queue_name));\nBEGIN\n IF left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';\n END IF;\n IF queue_name IS NULL OR queue_name !~ '^[a-z0-9][a-z0-9_-]{0,127}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN queue_name;\nEND;\n$$;\n\n-- Return-type changes require replacement. Do not CASCADE through user dependencies.\nDO $pgmq_receipt_types$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send(text,jsonb,integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send(text,jsonb,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send_batch(text,jsonb[],integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send_batch(text,jsonb[],integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.read(text,integer,integer)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.read(text,integer,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.pop(text)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.pop(text);\n END IF;\nEND\n$pgmq_receipt_types$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_seconds(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_SECONDS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_read_count(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 OR value > 10000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_COUNT_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message_id(value bigint)\nRETURNS bigint LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_ID_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.message_node_count(value jsonb)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n node_count integer;\n maximum_depth integer;\nBEGIN\n IF value IS NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF octet_length(convert_to(value::text, 'UTF8')) > 1048576 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n WITH RECURSIVE tree(item, depth) AS (\n SELECT value, 0\n UNION ALL\n SELECT child.item, tree.depth + 1\n FROM tree\n CROSS JOIN LATERAL (\n SELECT element AS item\n FROM jsonb_array_elements(CASE WHEN jsonb_typeof(tree.item) = 'array' THEN tree.item ELSE '[]'::jsonb END) AS element\n UNION ALL\n SELECT entry.value AS item\n FROM jsonb_each(CASE WHEN jsonb_typeof(tree.item) = 'object' THEN tree.item ELSE '{}'::jsonb END) AS entry\n ) AS child\n WHERE tree.depth < 65\n )\n SELECT count(*)::integer, max(depth) INTO node_count, maximum_depth\n FROM (SELECT depth FROM tree LIMIT 10001) AS bounded;\n IF node_count > 10000 OR maximum_depth > 64 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n RETURN node_count;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message(value jsonb)\nRETURNS jsonb LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n PERFORM pgmq_public.message_node_count(value);\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_messages(values_ jsonb[])\nRETURNS jsonb[] LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n item jsonb;\n total_bytes bigint := 0;\n total_nodes integer := 0;\nBEGIN\n IF values_ IS NULL OR cardinality(values_) < 1 OR cardinality(values_) > 10000\n OR array_ndims(values_) <> 1 OR array_lower(values_, 1) <> 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_BATCH_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH item IN ARRAY values_ LOOP\n total_nodes := total_nodes + pgmq_public.message_node_count(item);\n IF total_nodes > 100000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n total_bytes := total_bytes + octet_length(convert_to(item::text, 'UTF8'));\n IF total_bytes > 8388608 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n RETURN values_;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message(message), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), pgmq_public.require_messages(messages), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\n-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.\nCREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(\n queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0\n)\nRETURNS TABLE (msg_id text, created boolean)\nLANGUAGE plpgsql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue text := pgmq_public.require_public_queue(queue_name);\n captured_message jsonb := pgmq_public.require_message(message);\n captured_delay integer := pgmq_public.require_seconds(sleep_seconds);\n inserted boolean;\n sent_id bigint;\n existing_id bigint;\n existing_message jsonb;\n existing_delay integer;\nBEGIN\n IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';\n END IF;\n INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)\n VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)\n ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;\n inserted := FOUND;\n IF NOT inserted THEN\n SELECT keys.msg_id, keys.message, keys.sleep_seconds\n INTO existing_id, existing_message, existing_delay\n FROM supacloud_queue.job_keys AS keys\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n IF existing_id IS NULL OR existing_id < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';\n END IF;\n IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';\n END IF;\n -- This is a durable enqueue receipt, not evidence that the message is still pending.\n RETURN QUERY SELECT existing_id::text, false;\n RETURN;\n END IF;\n SELECT receipt.msg_id INTO STRICT sent_id\n FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);\n PERFORM pgmq_public.require_message_id(sent_id);\n UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n RETURN QUERY SELECT sent_id::text, true;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.read(pgmq_public.require_public_queue(queue_name),\n pgmq_public.require_seconds(sleep_seconds), pgmq_public.require_read_count(n)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.\"delete\"(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;\nGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;\n\nNOTIFY pgrst, 'reload schema';\n-- supacloud:sql-module:pgmq-public:end\n";
|
|
21
21
|
export declare const PGMQ_SQL = "\ncreate schema if not exists pgmq;\nrevoke all on schema pgmq from public, anon, authenticated;\ngrant usage on schema pgmq to service_role;\n\ncreate table if not exists pgmq.meta (\n queue_name text primary key,\n physical_name text not null unique,\n is_partitioned boolean not null default false,\n is_unlogged boolean not null default false,\n created_at timestamptz not null default now()\n);\n\n-- Preserve queues created by older Lite versions. Their physical names were\n-- the queue names, so only the already-truncated identifier can be recovered.\ninsert into pgmq.meta (queue_name, physical_name)\nselect substring(tablename from 3), substring(tablename from 3)\nfrom pg_tables\nwhere schemaname = 'pgmq' and left(tablename, 2) = 'q_'\non conflict do nothing;\n\ndo $$ begin\n if not exists (select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace\n where t.typname = 'message_record' and n.nspname = 'pgmq') then\n create type pgmq.message_record as (\n msg_id bigint, read_ct integer, enqueued_at timestamptz, vt timestamptz, message jsonb\n );\n end if;\nend $$;\n\ncreate or replace function pgmq._normalize_queue_name(queue_name text)\nreturns text language plpgsql immutable set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := btrim(queue_name);\nbegin\n if normalized is null or normalized !~ '^[a-z0-9][a-z0-9_-]{0,127}$' then\n raise exception 'invalid queue name: must match ^[a-z0-9][a-z0-9_-]{0,127}$'\n using errcode = '22023';\n end if;\n return normalized;\nend $pgmq$;\n\n-- PostgreSQL identifiers are limited to 63 bytes. Keep the conventional table\n-- name for short queues and add a deterministic hash for valid 62-128 byte\n-- public names so truncation can never merge two queues.\ncreate or replace function pgmq._physical_queue_name(queue_name text)\nreturns text language sql immutable set search_path = pgmq, pg_catalog, public as $pgmq$\n select case when length(normalized) <= 61 then normalized\n else left(normalized, 28) || '_' || md5(normalized) end\n from (select pgmq._normalize_queue_name(queue_name) as normalized) names;\n$pgmq$;\n\ncreate or replace function pgmq._resolve_queue(queue_name text)\nreturns text language plpgsql stable security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text;\nbegin\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n if physical is null then\n raise exception 'queue \"%\" does not exist', normalized using errcode = '42P01';\n end if;\n return physical;\nend $pgmq$;\n\ncreate or replace function pgmq.create(queue_name text) returns void language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text := pgmq._physical_queue_name(queue_name);\nbegin\n insert into pgmq.meta (queue_name, physical_name) values (normalized, physical)\n on conflict do nothing;\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n execute format('create table if not exists pgmq.%I (\n msg_id bigint generated always as identity primary key,\n read_ct integer not null default 0,\n enqueued_at timestamptz not null default now(),\n vt timestamptz not null default now(),\n message jsonb)', 'q_' || physical);\n execute format('create table if not exists pgmq.%I (\n msg_id bigint primary key, read_ct integer not null default 0,\n enqueued_at timestamptz not null, archived_at timestamptz not null default now(),\n vt timestamptz, message jsonb)', 'a_' || physical);\nend $pgmq$;\n\ncreate or replace function pgmq.send(queue_name text, msg jsonb, delay integer default 0)\nreturns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare id bigint; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || physical)\n into id using delay, msg;\n return id;\nend $pgmq$;\n\ncreate or replace function pgmq.send_batch(queue_name text, msgs jsonb[], delay integer default 0)\nreturns setof bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare queue_message jsonb;\nbegin\n perform pgmq._resolve_queue(queue_name);\n foreach queue_message in array msgs loop\n return next pgmq.send(queue_name, queue_message, delay);\n end loop;\nend $pgmq$;\n\ncreate or replace function pgmq.read(queue_name text, vt integer, qty integer)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n with cte as (\n select msg_id from pgmq.%I where vt <= now() order by msg_id limit $1 for update skip locked\n )\n update pgmq.%I m set vt = now() + make_interval(secs => $2), read_ct = read_ct + 1\n from cte where m.msg_id = cte.msg_id\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical, 'q_' || physical) using qty, vt;\nend $pgmq$;\n\ncreate or replace function pgmq.pop(queue_name text)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n with cte as (select msg_id from pgmq.%I where vt <= now() order by msg_id limit 1 for update skip locked)\n delete from pgmq.%I m using cte where m.msg_id = cte.msg_id\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical, 'q_' || physical);\nend $pgmq$;\n\ncreate or replace function pgmq.set_vt(queue_name text, msg_id bigint, vt_offset integer)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n update pgmq.%I m set vt = now() + make_interval(secs => $1)\n where m.msg_id = $2\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical) using vt_offset, msg_id;\nend $pgmq$;\n\ncreate or replace function pgmq.delete(queue_name text, msg_id bigint)\nreturns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n integer; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('delete from pgmq.%I where msg_id = $1', 'q_' || physical) using msg_id;\n get diagnostics n = row_count;\n return n > 0;\nend $pgmq$;\n\ncreate or replace function pgmq.archive(queue_name text, msg_id bigint)\nreturns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n integer; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format($fmt$\n with del as (delete from pgmq.%I where msg_id = $1 returning *)\n insert into pgmq.%I (msg_id, read_ct, enqueued_at, vt, message)\n select msg_id, read_ct, enqueued_at, vt, message from del\n $fmt$, 'q_' || physical, 'a_' || physical) using msg_id;\n get diagnostics n = row_count;\n return n > 0;\nend $pgmq$;\n\ncreate or replace function pgmq.drop_queue(queue_name text) returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text;\nbegin\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n if physical is null then return false; end if;\n execute format('drop table if exists pgmq.%I', 'q_' || physical);\n execute format('drop table if exists pgmq.%I', 'a_' || physical);\n delete from pgmq.meta where pgmq.meta.queue_name = normalized;\n return true;\nend $pgmq$;\n\ncreate or replace function pgmq.purge_queue(queue_name text) returns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n bigint; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('delete from pgmq.%I', 'q_' || physical);\n get diagnostics n = row_count;\n return n;\nend $pgmq$;\n\ncreate or replace function pgmq.list_queues()\nreturns table(queue_name text, is_partitioned boolean, is_unlogged boolean, created_at timestamptz)\nlanguage sql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\n select queue_name, is_partitioned, is_unlogged, created_at from pgmq.meta;\n$pgmq$;\n\nrevoke all on all functions in schema pgmq from public, anon, authenticated;\ngrant execute on all functions in schema pgmq to service_role;\n\n\n\n-- supacloud:sql-module:pgmq-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\n\nCREATE SCHEMA IF NOT EXISTS pgmq_public;\nGRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;\nCREATE SCHEMA IF NOT EXISTS supacloud_queue;\nCREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (\n queue_name text NOT NULL,\n job_key text NOT NULL,\n msg_id bigint NOT NULL,\n message jsonb NOT NULL,\n sleep_seconds integer NOT NULL,\n created_at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (queue_name, job_key)\n);\n-- Upgrade early key-only installations without inventing an input identity.\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;\nREVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)\nRETURNS text\nLANGUAGE plpgsql\nIMMUTABLE\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue_name text := lower(btrim(queue_name));\nBEGIN\n IF left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';\n END IF;\n IF queue_name IS NULL OR queue_name !~ '^[a-z0-9][a-z0-9_-]{0,127}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN queue_name;\nEND;\n$$;\n\n-- Return-type changes require replacement. Do not CASCADE through user dependencies.\nDO $pgmq_receipt_types$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send(text,jsonb,integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send(text,jsonb,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send_batch(text,jsonb[],integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send_batch(text,jsonb[],integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.read(text,integer,integer)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.read(text,integer,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.pop(text)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.pop(text);\n END IF;\nEND\n$pgmq_receipt_types$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_seconds(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_SECONDS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_read_count(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 OR value > 10000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_COUNT_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message_id(value bigint)\nRETURNS bigint LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_ID_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.message_node_count(value jsonb)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n node_count integer;\n maximum_depth integer;\nBEGIN\n IF value IS NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF octet_length(convert_to(value::text, 'UTF8')) > 1048576 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n WITH RECURSIVE tree(item, depth) AS (\n SELECT value, 0\n UNION ALL\n SELECT child.item, tree.depth + 1\n FROM tree\n CROSS JOIN LATERAL (\n SELECT element AS item\n FROM jsonb_array_elements(CASE WHEN jsonb_typeof(tree.item) = 'array' THEN tree.item ELSE '[]'::jsonb END) AS element\n UNION ALL\n SELECT entry.value AS item\n FROM jsonb_each(CASE WHEN jsonb_typeof(tree.item) = 'object' THEN tree.item ELSE '{}'::jsonb END) AS entry\n ) AS child\n WHERE tree.depth < 65\n )\n SELECT count(*)::integer, max(depth) INTO node_count, maximum_depth\n FROM (SELECT depth FROM tree LIMIT 10001) AS bounded;\n IF node_count > 10000 OR maximum_depth > 64 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n RETURN node_count;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message(value jsonb)\nRETURNS jsonb LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n PERFORM pgmq_public.message_node_count(value);\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_messages(values_ jsonb[])\nRETURNS jsonb[] LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n item jsonb;\n total_bytes bigint := 0;\n total_nodes integer := 0;\nBEGIN\n IF values_ IS NULL OR cardinality(values_) < 1 OR cardinality(values_) > 10000\n OR array_ndims(values_) <> 1 OR array_lower(values_, 1) <> 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_BATCH_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH item IN ARRAY values_ LOOP\n total_nodes := total_nodes + pgmq_public.message_node_count(item);\n IF total_nodes > 100000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n total_bytes := total_bytes + octet_length(convert_to(item::text, 'UTF8'));\n IF total_bytes > 8388608 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n RETURN values_;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message(message), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), pgmq_public.require_messages(messages), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\n-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.\nCREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(\n queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0\n)\nRETURNS TABLE (msg_id text, created boolean)\nLANGUAGE plpgsql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue text := pgmq_public.require_public_queue(queue_name);\n captured_message jsonb := pgmq_public.require_message(message);\n captured_delay integer := pgmq_public.require_seconds(sleep_seconds);\n inserted boolean;\n sent_id bigint;\n existing_id bigint;\n existing_message jsonb;\n existing_delay integer;\nBEGIN\n IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';\n END IF;\n INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)\n VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)\n ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;\n inserted := FOUND;\n IF NOT inserted THEN\n SELECT keys.msg_id, keys.message, keys.sleep_seconds\n INTO existing_id, existing_message, existing_delay\n FROM supacloud_queue.job_keys AS keys\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n IF existing_id IS NULL OR existing_id < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';\n END IF;\n IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';\n END IF;\n -- This is a durable enqueue receipt, not evidence that the message is still pending.\n RETURN QUERY SELECT existing_id::text, false;\n RETURN;\n END IF;\n SELECT receipt.msg_id INTO STRICT sent_id\n FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);\n PERFORM pgmq_public.require_message_id(sent_id);\n UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n RETURN QUERY SELECT sent_id::text, true;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.read(pgmq_public.require_public_queue(queue_name),\n pgmq_public.require_seconds(sleep_seconds), pgmq_public.require_read_count(n)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.\"delete\"(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;\nGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;\n\nNOTIFY pgrst, 'reload schema';\n-- supacloud:sql-module:pgmq-public:end\n";
|
|
22
|
-
export declare const WORKFLOWS_SQL = "\n-- supacloud:sql-module:workflows-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\nCREATE SCHEMA IF NOT EXISTS supacloud_workflows;\nREVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_workflows TO service_role;\n\nSELECT pgmq.create('supacloud_internal_workflows');\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.runs (\n id uuid PRIMARY KEY,\n workflow_name text NOT NULL\n CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n workflow_version text NOT NULL\n CHECK (char_length(workflow_version) BETWEEN 1 AND 80),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),\n created_at timestamptz NOT NULL DEFAULT now(),\n started_at timestamptz,\n completed_at timestamptz,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.steps (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_key text NOT NULL\n CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),\n retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400),\n queue_message_id bigint NOT NULL UNIQUE,\n claimed_by text,\n claimed_at timestamptz,\n completed_at timestamptz,\n next_step_key text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (run_id, step_key)\n);\n\nALTER TABLE supacloud_workflows.steps\n ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx\n ON supacloud_workflows.steps (run_id)\n WHERE status IN ('queued', 'running');\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx\n ON supacloud_workflows.runs (status, updated_at DESC, id);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx\n ON supacloud_workflows.steps (run_id, created_at, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.events (\n id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,\n event_type text NOT NULL\n CHECK (event_type IN (\n 'run_started', 'step_claimed', 'step_retried', 'step_completed',\n 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'\n )),\n attempt integer CHECK (attempt IS NULL OR attempt > 0),\n details jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(details) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx\n ON supacloud_workflows.events (run_id, id);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx\n ON supacloud_workflows.events (step_id, attempt)\n WHERE event_type IN ('step_retried', 'step_dead_lettered')\n AND details ->> 'operation' = 'retry';\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(\n p_run_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'runId', run.id,\n 'workflowName', run.workflow_name,\n 'workflowVersion', run.workflow_version,\n 'status', run.status,\n 'input', run.input,\n 'output', run.output,\n 'errorMessage', run.error_message,\n 'rowVersion', run.row_version::text,\n 'createdAt', run.created_at,\n 'startedAt', run.started_at,\n 'completedAt', run.completed_at,\n 'updatedAt', run.updated_at,\n 'idempotent', p_idempotent,\n 'steps', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'stepId', step.id,\n 'stepKey', step.step_key,\n 'status', step.status,\n 'input', step.input,\n 'output', step.output,\n 'errorMessage', step.error_message,\n 'attempts', step.attempts,\n 'maxAttempts', step.max_attempts,\n 'retryDelaySeconds', step.retry_delay_seconds,\n 'queueMessageId', step.queue_message_id::text,\n 'claimedBy', step.claimed_by,\n 'claimedAt', step.claimed_at,\n 'completedAt', step.completed_at,\n 'nextStepKey', step.next_step_key,\n 'createdAt', step.created_at,\n 'updatedAt', step.updated_at\n ) ORDER BY step.created_at, step.id)\n FROM supacloud_workflows.steps step\n WHERE step.run_id = run.id\n ), '[]'::jsonb)\n )\n FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(\n p_run_id uuid,\n p_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_step_key text := nullif(btrim(p_step_key), '');\n step_id uuid := gen_random_uuid();\n message_id bigint;\n created_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF normalized_step_key IS NULL\n OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id AND run.status IN ('queued', 'running')\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT queued_id INTO message_id\n FROM pgmq.send(\n 'supacloud_internal_workflows',\n jsonb_build_object('run_id', p_run_id, 'step_id', step_id),\n 0\n ) AS queued_id;\n\n INSERT INTO supacloud_workflows.steps (\n id, run_id, step_key, input, max_attempts, queue_message_id\n ) VALUES (\n step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id\n ) RETURNING * INTO created_step;\n\n RETURN created_step;\nEND;\n$$;\n\n-- Clean-code exception: private transitions keep typed PostgreSQL arguments and\n-- the complete lock/queue/ledger/event mutation in one transaction. The public\n-- contract already uses one JSON request; revisit if a private routine gains a\n-- second caller or any transition can be decomposed without weakening atomicity.\nCREATE OR REPLACE FUNCTION supacloud_workflows.start_run(\n p_run_id uuid,\n p_workflow_name text,\n p_workflow_version text,\n p_first_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_name text := nullif(btrim(p_workflow_name), '');\n normalized_version text := nullif(btrim(p_workflow_version), '');\n normalized_first_step_key text := nullif(btrim(p_first_step_key), '');\n existing_run supacloud_workflows.runs%ROWTYPE;\n existing_step supacloud_workflows.steps%ROWTYPE;\n first_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL\n OR normalized_name IS NULL\n OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_version IS NULL\n OR char_length(normalized_version) > 80\n OR normalized_first_step_key IS NULL\n OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;\n IF FOUND THEN\n SELECT * INTO existing_step\n FROM supacloud_workflows.steps\n WHERE run_id = p_run_id\n ORDER BY created_at, id\n LIMIT 1;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n IF existing_run.workflow_name <> normalized_name\n OR existing_run.workflow_version <> normalized_version\n OR existing_run.input <> p_input\n OR existing_step.step_key <> normalized_first_step_key\n OR existing_step.input <> p_input\n OR existing_step.max_attempts <> p_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n\n INSERT INTO supacloud_workflows.runs (\n id, workflow_name, workflow_version, input\n ) VALUES (\n p_run_id, normalized_name, normalized_version, p_input\n );\n first_step := supacloud_workflows.enqueue_step(\n p_run_id, normalized_first_step_key, p_input, p_max_attempts\n );\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)\n VALUES (p_run_id, first_step.id, 'run_started');\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(\n p_worker_id text,\n p_visibility_timeout_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n queued_message pgmq.message_record;\n message_run_id text;\n message_step_id text;\n candidate_run_id uuid;\n claimed_step supacloud_workflows.steps%ROWTYPE;\n claimed_run supacloud_workflows.runs%ROWTYPE;\nBEGIN\n IF normalized_worker_id IS NULL\n OR char_length(normalized_worker_id) > 200\n OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO queued_message\n FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)\n LIMIT 1;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n message_run_id := queued_message.message ->> 'run_id';\n message_step_id := queued_message.message ->> 'step_id';\n IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'\n OR message_run_id IS NULL\n OR message_step_id IS NULL\n OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'invalid_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id::text = lower(message_step_id)\n AND step.run_id::text = lower(message_run_id)\n AND step.queue_message_id = queued_message.msg_id;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';\n END IF;\n SELECT * INTO claimed_step\n FROM supacloud_workflows.steps\n WHERE id::text = lower(message_step_id)\n AND run_id = candidate_run_id\n AND queue_message_id = queued_message.msg_id\n FOR UPDATE;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT * INTO claimed_run\n FROM supacloud_workflows.runs\n WHERE id = claimed_step.run_id\n FOR UPDATE;\n IF claimed_run.status NOT IN ('queued', 'running')\n OR claimed_step.status NOT IN ('queued', 'running') THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'step_not_claimable',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF queued_message.read_ct > claimed_step.max_attempts THEN\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', attempts = queued_message.read_ct,\n error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = 'maximum attempts exceeded',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,\n jsonb_build_object('errorMessage', 'maximum attempts exceeded')\n );\n RETURN jsonb_build_object(\n 'status', 'dead_lettered',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts\n );\n END IF;\n\n UPDATE supacloud_workflows.steps\n SET status = 'running', attempts = queued_message.read_ct,\n retry_delay_seconds = 0, claimed_by = normalized_worker_id,\n claimed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'running', started_at = coalesce(started_at, now()),\n updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)\n VALUES (\n claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,\n jsonb_build_object('workerId', normalized_worker_id)\n );\n\n RETURN jsonb_build_object(\n 'status', 'claimed',\n 'runId', claimed_step.run_id,\n 'workflowName', claimed_run.workflow_name,\n 'workflowVersion', claimed_run.workflow_version,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'input', claimed_step.input,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts,\n 'workerId', normalized_worker_id\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(\n p_step_id uuid\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n candidate_run_id uuid;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id = p_step_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));\n SELECT * INTO active_step\n FROM supacloud_workflows.steps\n WHERE id = p_step_id\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n active_step supacloud_workflows.steps%ROWTYPE;\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\nBEGIN\n active_step := supacloud_workflows.lock_step(p_step_id);\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR active_step.queue_message_id IS DISTINCT FROM p_message_id\n OR active_step.attempts IS DISTINCT FROM p_attempt\n OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_output jsonb,\n p_next_step_key text,\n p_next_input jsonb,\n p_next_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n next_step supacloud_workflows.steps%ROWTYPE;\n normalized_next_step_key text := nullif(btrim(p_next_step_key), '');\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'\n OR normalized_next_step_key IS NULL\n OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n\n IF current_step.status = 'completed' THEN\n SELECT * INTO next_step\n FROM supacloud_workflows.steps\n WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;\n IF NOT FOUND\n OR current_step.output IS DISTINCT FROM p_output\n OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key\n OR next_step.input IS DISTINCT FROM p_next_input\n OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_output, error_message = '',\n completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()\n WHERE id = current_step.id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_completed', p_attempt,\n jsonb_build_object('nextStepKey', normalized_next_step_key)\n );\n next_step := supacloud_workflows.enqueue_step(\n current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_step_output jsonb,\n p_run_output jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT * INTO current_run\n FROM supacloud_workflows.runs\n WHERE id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'completed' THEN\n IF current_step.next_step_key IS NOT NULL\n OR current_step.output IS DISTINCT FROM p_step_output\n OR current_run.status IS DISTINCT FROM 'completed'\n OR current_run.output IS DISTINCT FROM p_run_output THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_step_output, error_message = '',\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'completed', output = p_run_output, error_message = '',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text,\n p_delay_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n retry_receipt jsonb;\n queue_message_updated boolean;\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000\n OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step(p_step_id);\n SELECT event.details INTO retry_receipt\n FROM supacloud_workflows.events event\n WHERE event.step_id = current_step.id\n AND event.attempt = p_attempt\n AND event.event_type IN ('step_retried', 'step_dead_lettered')\n AND event.details ->> 'operation' = 'retry'\n ORDER BY event.id DESC\n LIMIT 1;\n IF FOUND THEN\n IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text\n OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id\n OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error\n OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n retry_receipt := retry_receipt || jsonb_build_object(\n 'stepId', current_step.id,\n 'attempt', p_attempt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, true)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR current_step.queue_message_id IS DISTINCT FROM p_message_id\n OR current_step.attempts IS DISTINCT FROM p_attempt\n OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id\n OR current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n IF current_step.attempts >= current_step.max_attempts THEN\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n\n SELECT EXISTS (\n SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)\n ) INTO queue_message_updated;\n IF queue_message_updated IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'queued', error_message = normalized_error,\n retry_delay_seconds = p_delay_seconds, updated_at = now()\n WHERE id = current_step.id;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_retried', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT * INTO current_run\n FROM supacloud_workflows.runs\n WHERE id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'failed' THEN\n IF current_step.error_message IS DISTINCT FROM normalized_error\n OR current_run.status IS DISTINCT FROM 'failed'\n OR current_run.error_message IS DISTINCT FROM normalized_error THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_failed', p_attempt,\n jsonb_build_object('errorMessage', normalized_error)\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(\n p_run_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_reason text := nullif(btrim(p_reason), '');\n locked_run supacloud_workflows.runs%ROWTYPE;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n IF locked_run.status = 'cancelled' THEN\n IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n IF locked_run.status NOT IN ('queued', 'running') THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n SELECT * INTO active_step\n FROM supacloud_workflows.steps\n WHERE run_id = p_run_id AND status IN ('queued', 'running')\n FOR UPDATE;\n IF FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);\n UPDATE supacloud_workflows.steps\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now()\n WHERE id = active_step.id;\n END IF;\n UPDATE supacloud_workflows.runs\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = p_run_id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, details\n ) VALUES (\n p_run_id, active_step.id, 'run_cancelled',\n jsonb_build_object('reason', normalized_reason)\n );\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.run_events(\n p_run_id uuid,\n p_after_event_id bigint,\n p_limit integer\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'eventId', page.id::text,\n 'runId', page.run_id,\n 'stepId', page.step_id,\n 'eventType', page.event_type,\n 'attempt', page.attempt,\n 'details', page.details,\n 'createdAt', page.created_at\n ) ORDER BY page.id), '[]'::jsonb)\n FROM (\n SELECT event.*\n FROM supacloud_workflows.events event\n WHERE event.run_id = p_run_id AND event.id > p_after_event_id\n ORDER BY event.id\n LIMIT p_limit\n ) page\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(\n request jsonb,\n key text\n) RETURNS uuid\nLANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$\nDECLARE\n uuid_text text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n uuid_text := request ->> key;\n IF uuid_text IS NULL\n OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN uuid_text::uuid;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n max_attempts integer;\nBEGIN\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n RETURN supacloud_workflows.start_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'workflowName',\n request ->> 'workflowVersion',\n request ->> 'firstStepKey',\n coalesce(request -> 'input', '{}'::jsonb),\n max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n visibility_timeout_seconds integer;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);\n RETURN supacloud_workflows.claim_step(\n request ->> 'workerId', visibility_timeout_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n next_max_attempts integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.advance_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'output', '{}'::jsonb),\n request ->> 'nextStepKey',\n coalesce(request -> 'nextInput', '{}'::jsonb),\n next_max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.complete_run(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'stepOutput', '{}'::jsonb),\n coalesce(request -> 'runOutput', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n delay_seconds integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.retry_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage',\n delay_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.fail_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.cancel_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'reason'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.snapshot(\n supacloud_workflows.request_uuid(request, 'runId'), false\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n after_event_id bigint;\n event_limit integer;\nBEGIN\n after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);\n event_limit := coalesce((request ->> 'limit')::integer, 100);\n IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.run_events(\n supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\n\nREVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;\n-- supacloud:sql-module:workflows-public:end\n\n-- supacloud:sql-module:commands-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_commands;\nREVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_commands TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_commands.receipts (\n id uuid PRIMARY KEY,\n command_type text NOT NULL\n CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_type text NOT NULL\n CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),\n actor_id uuid,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(payload) = 'object'),\n payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),\n workflow_run_id uuid NOT NULL UNIQUE\n REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_commands_target_idx\n ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);\n\nALTER TABLE supacloud_commands.receipts ADD COLUMN IF NOT EXISTS tenant_id text;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.snapshot(\n p_command_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'commandId', receipt.id,\n 'commandType', receipt.command_type,\n 'targetType', receipt.target_type,\n 'targetId', receipt.target_id,\n 'actorId', receipt.actor_id,\n 'tenantId', receipt.tenant_id,\n 'payloadFingerprint', receipt.payload_fingerprint,\n 'createdAt', receipt.created_at,\n 'idempotent', p_idempotent,\n 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)\n )\n FROM supacloud_commands.receipts receipt\n WHERE receipt.id = p_command_id\n$$;\n\n-- Application-owned SECURITY DEFINER RPCs may call this after their domain\n-- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue\n-- together, so a lost response can be replayed with the same command ID.\nDROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);\nCREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n command_id uuid;\n actor_id uuid;\n max_attempts integer;\n payload jsonb;\n normalized_command_type text;\n normalized_target_type text;\n normalized_target_id text;\n fingerprint text;\n existing supacloud_commands.receipts%ROWTYPE;\n tenant_id text;\n existing_execution jsonb;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id := command_id_text::uuid;\n tenant_id := request->>'tenantId';\n IF request ? 'tenantId' AND (jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR tenant_id !~ '^[A-Za-z0-9._:@|-]{1,200}$') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN\n actor_id := (request ->> 'actorId')::uuid;\n END IF;\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n payload := coalesce(request -> 'payload', '{}'::jsonb);\n normalized_command_type := nullif(btrim(request ->> 'commandType'), '');\n normalized_target_type := nullif(btrim(request ->> 'targetType'), '');\n normalized_target_id := nullif(btrim(request ->> 'targetId'), '');\n IF normalized_command_type IS NULL\n OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_type IS NULL\n OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_id IS NULL\n OR char_length(normalized_target_id) > 500\n OR jsonb_typeof(payload) IS DISTINCT FROM 'object'\n OR max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n\n fingerprint := 'md5:' || md5(payload::text);\n PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));\n SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;\n IF FOUND THEN\n IF existing.command_type <> normalized_command_type\n OR existing.target_type <> normalized_target_type\n OR existing.target_id <> normalized_target_id\n OR existing.actor_id IS DISTINCT FROM actor_id\n OR existing.tenant_id IS DISTINCT FROM tenant_id\n OR existing.payload <> payload\n OR existing.payload_fingerprint <> fingerprint THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_commands.snapshot(command_id, true);\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO existing_execution\n USING jsonb_build_object('commandId',command_id);\n IF existing_execution IS NOT NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n END IF;\n\n PERFORM supacloud_workflows.start_run(\n command_id,\n 'command.' || normalized_command_type,\n '1',\n 'execute',\n jsonb_build_object(\n 'commandId', command_id,\n 'commandType', normalized_command_type,\n 'targetType', normalized_target_type,\n 'targetId', normalized_target_id,\n 'actorId', actor_id,\n 'payload', payload,\n 'payloadFingerprint', fingerprint\n ),\n max_attempts\n );\n\n INSERT INTO supacloud_commands.receipts (\n id, command_type, target_type, target_id, actor_id, payload,\n payload_fingerprint, workflow_run_id, tenant_id\n ) VALUES (\n command_id, normalized_command_type, normalized_target_type,\n normalized_target_id, actor_id, payload, fingerprint, command_id, tenant_id\n );\n RETURN supacloud_commands.snapshot(command_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.status(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n execution jsonb;\n submission jsonb;\n field text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'commandId' THEN\n IF jsonb_typeof(request->'commandId') IS DISTINCT FROM 'string'\n OR request->>'commandId' !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k <> 'commandId') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n request := jsonb_build_object('commandId',(request->>'commandId')::uuid::text);\n ELSE\n IF EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k NOT IN ('tenantId','actorId','command','operationId')) THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH field IN ARRAY ARRAY['tenantId','actorId','command','operationId'] LOOP\n IF jsonb_typeof(request->field) IS DISTINCT FROM 'string' OR request->>field !~ '^[A-Za-z0-9._:@|-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO execution USING request;\n IF execution IS NOT NULL THEN RETURN execution; END IF;\n END IF;\n IF NOT (request ? 'commandId') THEN\n IF jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'actorId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'command') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'operationId') IS DISTINCT FROM 'string' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN NULL;\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n submission := supacloud_commands.snapshot(command_id_text::uuid, false);\n IF submission IS NULL THEN RETURN NULL; END IF;\n RETURN jsonb_build_object('kind','submission','commandId',command_id_text::uuid,\n 'execution',NULL,'workflow',jsonb_build_object(\n 'runId',submission->'workflow'->'runId','status',submission->'workflow'->'status'));\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n PERFORM supacloud_commands.submit(request);\n RETURN supacloud_commands.status(jsonb_build_object('commandId',request->>'commandId'));\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_commands.status(request)\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;\n-- supacloud:sql-module:commands-public:end\n\n-- supacloud:sql-module:artifacts-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_artifacts;\nREVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (\n id uuid PRIMARY KEY,\n storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,\n bucket_id text NOT NULL,\n object_path text NOT NULL,\n object_version text NOT NULL,\n artifact_type text NOT NULL\n CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),\n size_bytes bigint NOT NULL CHECK (size_bytes >= 0),\n mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n retention_until timestamptz,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n CHECK (retention_until IS NULL OR retention_until >= created_at),\n UNIQUE (bucket_id, object_path, object_version)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx\n ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (\n parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n relation_type text NOT NULL\n CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),\n CHECK (parent_artifact_id <> child_artifact_id)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx\n ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()\nRETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.storage_object_id = OLD.id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';\n END IF;\n RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;\nCREATE TRIGGER supacloud_artifact_storage_fence\nBEFORE UPDATE OR DELETE ON storage.objects\nFOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(\n p_artifact_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'artifactId', artifact.id,\n 'bucketId', artifact.bucket_id,\n 'objectPath', artifact.object_path,\n 'objectVersion', artifact.object_version,\n 'artifactType', artifact.artifact_type,\n 'sha256', artifact.sha256,\n 'sizeBytes', artifact.size_bytes::text,\n 'mimeType', artifact.mime_type,\n 'metadata', artifact.metadata,\n 'retentionUntil', artifact.retention_until,\n 'createdBy', artifact.created_by,\n 'createdAt', artifact.created_at,\n 'idempotent', p_idempotent,\n 'parents', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'artifactId', edge.parent_artifact_id,\n 'relationType', edge.relation_type,\n 'metadata', edge.metadata,\n 'createdAt', edge.created_at\n ) ORDER BY edge.created_at, edge.parent_artifact_id)\n FROM supacloud_artifacts.lineage edge\n WHERE edge.child_artifact_id = artifact.id\n ), '[]'::jsonb)\n )\n FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.id = p_artifact_id\n$$;\n\nDROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);\nCREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n artifact_id uuid;\n size_bytes bigint;\n metadata jsonb;\n retention_until timestamptz;\n created_by uuid;\n normalized_bucket text;\n normalized_path text;\n normalized_type text;\n normalized_sha256 text;\n normalized_mime text;\n object_row storage.objects%ROWTYPE;\n existing supacloud_artifacts.artifacts%ROWTYPE;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n artifact_id := (request ->> 'artifactId')::uuid;\n size_bytes := (request ->> 'sizeBytes')::bigint;\n metadata := coalesce(request -> 'metadata', '{}'::jsonb);\n retention_until := (request ->> 'retentionUntil')::timestamptz;\n created_by := (request ->> 'createdBy')::uuid;\n normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');\n normalized_path := nullif(btrim(request ->> 'objectPath'), '');\n normalized_type := nullif(btrim(request ->> 'artifactType'), '');\n normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));\n normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));\n IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL\n OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'\n OR normalized_path ~ '(^|/)(.|..)(/|$)'\n OR normalized_type IS NULL\n OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'\n OR size_bytes IS NULL OR size_bytes < 0\n OR normalized_mime IS NULL OR char_length(normalized_mime) > 255\n OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'\n OR (retention_until IS NOT NULL AND retention_until < now()) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO object_row FROM storage.objects\n WHERE bucket_id = normalized_bucket AND name = normalized_path;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));\n SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;\n IF FOUND THEN\n IF existing.storage_object_id <> object_row.id\n OR existing.artifact_type <> normalized_type\n OR existing.sha256 <> normalized_sha256\n OR existing.size_bytes <> size_bytes\n OR existing.mime_type <> normalized_mime\n OR existing.metadata <> metadata\n OR existing.retention_until IS DISTINCT FROM retention_until\n OR existing.created_by IS DISTINCT FROM created_by THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(artifact_id, true);\n END IF;\n\n INSERT INTO supacloud_artifacts.artifacts (\n id, storage_object_id, bucket_id, object_path, object_version,\n artifact_type, sha256, size_bytes, mime_type, metadata,\n retention_until, created_by\n ) VALUES (\n artifact_id, object_row.id, normalized_bucket, normalized_path,\n object_row.version::text, normalized_type, normalized_sha256,\n size_bytes, normalized_mime, metadata, retention_until, created_by\n );\n RETURN supacloud_artifacts.snapshot(artifact_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.link(\n p_parent_artifact_id uuid,\n p_child_artifact_id uuid,\n p_relation_type text,\n p_metadata jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_relation text := nullif(btrim(p_relation_type), '');\n inserted_count integer;\nBEGIN\n IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL\n OR p_parent_artifact_id = p_child_artifact_id\n OR normalized_relation IS NULL\n OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (\n WITH RECURSIVE descendants(artifact_id) AS (\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n WHERE edge.parent_artifact_id = p_child_artifact_id\n UNION\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n JOIN descendants current ON edge.parent_artifact_id = current.artifact_id\n )\n SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';\n END IF;\n INSERT INTO supacloud_artifacts.lineage (\n parent_artifact_id, child_artifact_id, relation_type, metadata\n ) VALUES (\n p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata\n ) ON CONFLICT DO NOTHING;\n GET DIAGNOSTICS inserted_count = ROW_COUNT;\n IF inserted_count = 0 AND NOT EXISTS (\n SELECT 1 FROM supacloud_artifacts.lineage\n WHERE parent_artifact_id = p_parent_artifact_id\n AND child_artifact_id = p_child_artifact_id\n AND relation_type = normalized_relation\n AND metadata = p_metadata\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)\nRETURNS jsonb\nLANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_artifacts.register(request)\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.link(\n (request ->> 'parentArtifactId')::uuid,\n (request ->> 'childArtifactId')::uuid,\n request ->> 'relationType',\n coalesce(request -> 'metadata', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;\n-- supacloud:sql-module:artifacts-public:end\n";
|
|
22
|
+
export declare const WORKFLOWS_SQL = "\n-- supacloud:sql-module:workflows-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\nCREATE SCHEMA IF NOT EXISTS supacloud_workflows;\nREVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_workflows TO service_role;\n\nSELECT pgmq.create('supacloud_internal_workflows');\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.runs (\n id uuid PRIMARY KEY,\n workflow_name text NOT NULL\n CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n workflow_version text NOT NULL\n CHECK (char_length(workflow_version) BETWEEN 1 AND 80),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),\n created_at timestamptz NOT NULL DEFAULT now(),\n started_at timestamptz,\n completed_at timestamptz,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.steps (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_key text NOT NULL\n CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),\n retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400),\n queue_message_id bigint NOT NULL UNIQUE,\n claimed_by text,\n claimed_at timestamptz,\n completed_at timestamptz,\n next_step_key text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (run_id, step_key)\n);\n\nALTER TABLE supacloud_workflows.steps\n ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx\n ON supacloud_workflows.steps (run_id)\n WHERE status IN ('queued', 'running');\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx\n ON supacloud_workflows.runs (status, updated_at DESC, id);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx\n ON supacloud_workflows.steps (run_id, created_at, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.events (\n id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,\n event_type text NOT NULL\n CHECK (event_type IN (\n 'run_started', 'step_claimed', 'step_retried', 'step_completed',\n 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'\n )),\n attempt integer CHECK (attempt IS NULL OR attempt > 0),\n details jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(details) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx\n ON supacloud_workflows.events (run_id, id);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx\n ON supacloud_workflows.events (step_id, attempt)\n WHERE event_type IN ('step_retried', 'step_dead_lettered')\n AND details ->> 'operation' = 'retry';\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(\n p_run_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'runId', run.id,\n 'workflowName', run.workflow_name,\n 'workflowVersion', run.workflow_version,\n 'status', run.status,\n 'input', run.input,\n 'output', run.output,\n 'errorMessage', run.error_message,\n 'rowVersion', run.row_version::text,\n 'createdAt', run.created_at,\n 'startedAt', run.started_at,\n 'completedAt', run.completed_at,\n 'updatedAt', run.updated_at,\n 'idempotent', p_idempotent,\n 'steps', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'stepId', step.id,\n 'stepKey', step.step_key,\n 'status', step.status,\n 'input', step.input,\n 'output', step.output,\n 'errorMessage', step.error_message,\n 'attempts', step.attempts,\n 'maxAttempts', step.max_attempts,\n 'retryDelaySeconds', step.retry_delay_seconds,\n 'queueMessageId', step.queue_message_id::text,\n 'claimedBy', step.claimed_by,\n 'claimedAt', step.claimed_at,\n 'completedAt', step.completed_at,\n 'nextStepKey', step.next_step_key,\n 'createdAt', step.created_at,\n 'updatedAt', step.updated_at\n ) ORDER BY step.created_at, step.id)\n FROM supacloud_workflows.steps step\n WHERE step.run_id = run.id\n ), '[]'::jsonb)\n )\n FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(\n p_run_id uuid,\n p_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_step_key text := nullif(btrim(p_step_key), '');\n step_id uuid := gen_random_uuid();\n message_id bigint;\n created_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF normalized_step_key IS NULL\n OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id AND run.status IN ('queued', 'running')\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT queued_id INTO message_id\n FROM pgmq.send(\n 'supacloud_internal_workflows',\n jsonb_build_object('run_id', p_run_id, 'step_id', step_id),\n 0\n ) AS queued_id;\n\n INSERT INTO supacloud_workflows.steps (\n id, run_id, step_key, input, max_attempts, queue_message_id\n ) VALUES (\n step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id\n ) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;\n\n RETURN created_step;\nEND;\n$$;\n\n-- Clean-code exception: private transitions keep typed PostgreSQL arguments and\n-- the complete lock/queue/ledger/event mutation in one transaction. The public\n-- contract already uses one JSON request; revisit if a private routine gains a\n-- second caller or any transition can be decomposed without weakening atomicity.\nCREATE OR REPLACE FUNCTION supacloud_workflows.start_run(\n p_run_id uuid,\n p_workflow_name text,\n p_workflow_version text,\n p_first_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_name text := nullif(btrim(p_workflow_name), '');\n normalized_version text := nullif(btrim(p_workflow_version), '');\n normalized_first_step_key text := nullif(btrim(p_first_step_key), '');\n existing_run supacloud_workflows.runs%ROWTYPE;\n existing_step supacloud_workflows.steps%ROWTYPE;\n first_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL\n OR normalized_name IS NULL\n OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_version IS NULL\n OR char_length(normalized_version) > 80\n OR normalized_first_step_key IS NULL\n OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;\n IF FOUND THEN\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = p_run_id\n ORDER BY created_at, id\n LIMIT 1;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n IF existing_run.workflow_name <> normalized_name\n OR existing_run.workflow_version <> normalized_version\n OR existing_run.input <> p_input\n OR existing_step.step_key <> normalized_first_step_key\n OR existing_step.input <> p_input\n OR existing_step.max_attempts <> p_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n\n INSERT INTO supacloud_workflows.runs (\n id, workflow_name, workflow_version, input\n ) VALUES (\n p_run_id, normalized_name, normalized_version, p_input\n );\n first_step := supacloud_workflows.enqueue_step(\n p_run_id, normalized_first_step_key, p_input, p_max_attempts\n );\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)\n VALUES (p_run_id, first_step.id, 'run_started');\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(\n p_worker_id text,\n p_visibility_timeout_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n queued_message pgmq.message_record;\n message_run_id text;\n message_step_id text;\n candidate_run_id uuid;\n claimed_step supacloud_workflows.steps%ROWTYPE;\n claimed_run supacloud_workflows.runs%ROWTYPE;\nBEGIN\n IF normalized_worker_id IS NULL\n OR char_length(normalized_worker_id) > 200\n OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO queued_message\n FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)\n LIMIT 1;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n message_run_id := queued_message.message ->> 'run_id';\n message_step_id := queued_message.message ->> 'step_id';\n IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'\n OR message_run_id IS NULL\n OR message_step_id IS NULL\n OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'invalid_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id::text = lower(message_step_id)\n AND step.run_id::text = lower(message_run_id)\n AND step.queue_message_id = queued_message.msg_id;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';\n END IF;\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.id::text = lower(message_step_id)\n AND workflow_step.run_id = candidate_run_id\n AND workflow_step.queue_message_id = queued_message.msg_id\n FOR UPDATE;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = claimed_step.run_id\n FOR UPDATE;\n IF claimed_run.status NOT IN ('queued', 'running')\n OR claimed_step.status NOT IN ('queued', 'running') THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'step_not_claimable',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF queued_message.read_ct > claimed_step.max_attempts THEN\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', attempts = queued_message.read_ct,\n error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = 'maximum attempts exceeded',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,\n jsonb_build_object('errorMessage', 'maximum attempts exceeded')\n );\n RETURN jsonb_build_object(\n 'status', 'dead_lettered',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts\n );\n END IF;\n\n UPDATE supacloud_workflows.steps\n SET status = 'running', attempts = queued_message.read_ct,\n retry_delay_seconds = 0, claimed_by = normalized_worker_id,\n claimed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'running', started_at = coalesce(started_at, now()),\n updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)\n VALUES (\n claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,\n jsonb_build_object('workerId', normalized_worker_id)\n );\n\n RETURN jsonb_build_object(\n 'status', 'claimed',\n 'runId', claimed_step.run_id,\n 'workflowName', claimed_run.workflow_name,\n 'workflowVersion', claimed_run.workflow_version,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'input', claimed_step.input,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts,\n 'workerId', normalized_worker_id\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(\n p_step_id uuid\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n candidate_run_id uuid;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id = p_step_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.id = p_step_id\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n active_step supacloud_workflows.steps%ROWTYPE;\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\nBEGIN\n active_step := supacloud_workflows.lock_step(p_step_id);\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR active_step.queue_message_id IS DISTINCT FROM p_message_id\n OR active_step.attempts IS DISTINCT FROM p_attempt\n OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_output jsonb,\n p_next_step_key text,\n p_next_input jsonb,\n p_next_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n next_step supacloud_workflows.steps%ROWTYPE;\n normalized_next_step_key text := nullif(btrim(p_next_step_key), '');\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'\n OR normalized_next_step_key IS NULL\n OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n\n IF current_step.status = 'completed' THEN\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;\n IF NOT FOUND\n OR current_step.output IS DISTINCT FROM p_output\n OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key\n OR next_step.input IS DISTINCT FROM p_next_input\n OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_output, error_message = '',\n completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()\n WHERE id = current_step.id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_completed', p_attempt,\n jsonb_build_object('nextStepKey', normalized_next_step_key)\n );\n next_step := supacloud_workflows.enqueue_step(\n current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_step_output jsonb,\n p_run_output jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'completed' THEN\n IF current_step.next_step_key IS NOT NULL\n OR current_step.output IS DISTINCT FROM p_step_output\n OR current_run.status IS DISTINCT FROM 'completed'\n OR current_run.output IS DISTINCT FROM p_run_output THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_step_output, error_message = '',\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'completed', output = p_run_output, error_message = '',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text,\n p_delay_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n retry_receipt jsonb;\n queue_message_updated boolean;\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000\n OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step(p_step_id);\n SELECT event.details INTO retry_receipt\n FROM supacloud_workflows.events event\n WHERE event.step_id = current_step.id\n AND event.attempt = p_attempt\n AND event.event_type IN ('step_retried', 'step_dead_lettered')\n AND event.details ->> 'operation' = 'retry'\n ORDER BY event.id DESC\n LIMIT 1;\n IF FOUND THEN\n IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text\n OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id\n OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error\n OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n retry_receipt := retry_receipt || jsonb_build_object(\n 'stepId', current_step.id,\n 'attempt', p_attempt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, true)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR current_step.queue_message_id IS DISTINCT FROM p_message_id\n OR current_step.attempts IS DISTINCT FROM p_attempt\n OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id\n OR current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n IF current_step.attempts >= current_step.max_attempts THEN\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n\n SELECT EXISTS (\n SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)\n ) INTO queue_message_updated;\n IF queue_message_updated IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'queued', error_message = normalized_error,\n retry_delay_seconds = p_delay_seconds, updated_at = now()\n WHERE id = current_step.id;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_retried', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'failed' THEN\n IF current_step.error_message IS DISTINCT FROM normalized_error\n OR current_run.status IS DISTINCT FROM 'failed'\n OR current_run.error_message IS DISTINCT FROM normalized_error THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_failed', p_attempt,\n jsonb_build_object('errorMessage', normalized_error)\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(\n p_run_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_reason text := nullif(btrim(p_reason), '');\n locked_run supacloud_workflows.runs%ROWTYPE;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n IF locked_run.status = 'cancelled' THEN\n IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n IF locked_run.status NOT IN ('queued', 'running') THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')\n FOR UPDATE;\n IF FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);\n UPDATE supacloud_workflows.steps\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now()\n WHERE id = active_step.id;\n END IF;\n UPDATE supacloud_workflows.runs\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = p_run_id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, details\n ) VALUES (\n p_run_id, active_step.id, 'run_cancelled',\n jsonb_build_object('reason', normalized_reason)\n );\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.run_events(\n p_run_id uuid,\n p_after_event_id bigint,\n p_limit integer\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'eventId', page.id::text,\n 'runId', page.run_id,\n 'stepId', page.step_id,\n 'eventType', page.event_type,\n 'attempt', page.attempt,\n 'details', page.details,\n 'createdAt', page.created_at\n ) ORDER BY page.id), '[]'::jsonb)\n FROM (\n SELECT event.*\n FROM supacloud_workflows.events event\n WHERE event.run_id = p_run_id AND event.id > p_after_event_id\n ORDER BY event.id\n LIMIT p_limit\n ) page\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(\n request jsonb,\n key text\n) RETURNS uuid\nLANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$\nDECLARE\n uuid_text text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n uuid_text := request ->> key;\n IF uuid_text IS NULL\n OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN uuid_text::uuid;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n max_attempts integer;\nBEGIN\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n RETURN supacloud_workflows.start_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'workflowName',\n request ->> 'workflowVersion',\n request ->> 'firstStepKey',\n coalesce(request -> 'input', '{}'::jsonb),\n max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n visibility_timeout_seconds integer;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);\n RETURN supacloud_workflows.claim_step(\n request ->> 'workerId', visibility_timeout_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n next_max_attempts integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.advance_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'output', '{}'::jsonb),\n request ->> 'nextStepKey',\n coalesce(request -> 'nextInput', '{}'::jsonb),\n next_max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.complete_run(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'stepOutput', '{}'::jsonb),\n coalesce(request -> 'runOutput', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n delay_seconds integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.retry_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage',\n delay_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.fail_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.cancel_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'reason'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.snapshot(\n supacloud_workflows.request_uuid(request, 'runId'), false\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n after_event_id bigint;\n event_limit integer;\nBEGIN\n after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);\n event_limit := coalesce((request ->> 'limit')::integer, 100);\n IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.run_events(\n supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\n\nREVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;\n-- supacloud:sql-module:workflows-public:end\n\n-- supacloud:sql-module:commands-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_commands;\nREVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_commands TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_commands.receipts (\n id uuid PRIMARY KEY,\n command_type text NOT NULL\n CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_type text NOT NULL\n CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),\n actor_id uuid,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(payload) = 'object'),\n payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),\n workflow_run_id uuid NOT NULL UNIQUE\n REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_commands_target_idx\n ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);\n\nALTER TABLE supacloud_commands.receipts ADD COLUMN IF NOT EXISTS tenant_id text;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.snapshot(\n p_command_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'commandId', receipt.id,\n 'commandType', receipt.command_type,\n 'targetType', receipt.target_type,\n 'targetId', receipt.target_id,\n 'actorId', receipt.actor_id,\n 'tenantId', receipt.tenant_id,\n 'payloadFingerprint', receipt.payload_fingerprint,\n 'createdAt', receipt.created_at,\n 'idempotent', p_idempotent,\n 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)\n )\n FROM supacloud_commands.receipts receipt\n WHERE receipt.id = p_command_id\n$$;\n\n-- Application-owned SECURITY DEFINER RPCs may call this after their domain\n-- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue\n-- together, so a lost response can be replayed with the same command ID.\nDROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);\nCREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n command_id uuid;\n actor_id uuid;\n max_attempts integer;\n payload jsonb;\n normalized_command_type text;\n normalized_target_type text;\n normalized_target_id text;\n fingerprint text;\n existing supacloud_commands.receipts%ROWTYPE;\n tenant_id text;\n existing_execution jsonb;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id := command_id_text::uuid;\n tenant_id := request->>'tenantId';\n IF request ? 'tenantId' AND (jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR tenant_id !~ '^[A-Za-z0-9._:@|-]{1,200}$') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN\n actor_id := (request ->> 'actorId')::uuid;\n END IF;\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n payload := coalesce(request -> 'payload', '{}'::jsonb);\n normalized_command_type := nullif(btrim(request ->> 'commandType'), '');\n normalized_target_type := nullif(btrim(request ->> 'targetType'), '');\n normalized_target_id := nullif(btrim(request ->> 'targetId'), '');\n IF normalized_command_type IS NULL\n OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_type IS NULL\n OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_id IS NULL\n OR char_length(normalized_target_id) > 500\n OR jsonb_typeof(payload) IS DISTINCT FROM 'object'\n OR max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n\n fingerprint := 'md5:' || md5(payload::text);\n PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));\n SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;\n IF FOUND THEN\n IF existing.command_type <> normalized_command_type\n OR existing.target_type <> normalized_target_type\n OR existing.target_id <> normalized_target_id\n OR existing.actor_id IS DISTINCT FROM actor_id\n OR existing.tenant_id IS DISTINCT FROM tenant_id\n OR existing.payload <> payload\n OR existing.payload_fingerprint <> fingerprint THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_commands.snapshot(command_id, true);\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO existing_execution\n USING jsonb_build_object('commandId',command_id);\n IF existing_execution IS NOT NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n END IF;\n\n PERFORM supacloud_workflows.start_run(\n command_id,\n 'command.' || normalized_command_type,\n '1',\n 'execute',\n jsonb_build_object(\n 'commandId', command_id,\n 'commandType', normalized_command_type,\n 'targetType', normalized_target_type,\n 'targetId', normalized_target_id,\n 'actorId', actor_id,\n 'payload', payload,\n 'payloadFingerprint', fingerprint\n ),\n max_attempts\n );\n\n INSERT INTO supacloud_commands.receipts (\n id, command_type, target_type, target_id, actor_id, payload,\n payload_fingerprint, workflow_run_id, tenant_id\n ) VALUES (\n command_id, normalized_command_type, normalized_target_type,\n normalized_target_id, actor_id, payload, fingerprint, command_id, tenant_id\n );\n RETURN supacloud_commands.snapshot(command_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.status(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n execution jsonb;\n submission jsonb;\n field text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'commandId' THEN\n IF jsonb_typeof(request->'commandId') IS DISTINCT FROM 'string'\n OR request->>'commandId' !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k <> 'commandId') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n request := jsonb_build_object('commandId',(request->>'commandId')::uuid::text);\n ELSE\n IF EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k NOT IN ('tenantId','actorId','command','operationId')) THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH field IN ARRAY ARRAY['tenantId','actorId','command','operationId'] LOOP\n IF jsonb_typeof(request->field) IS DISTINCT FROM 'string' OR request->>field !~ '^[A-Za-z0-9._:@|-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO execution USING request;\n IF execution IS NOT NULL THEN RETURN execution; END IF;\n END IF;\n IF NOT (request ? 'commandId') THEN\n IF jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'actorId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'command') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'operationId') IS DISTINCT FROM 'string' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN NULL;\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n submission := supacloud_commands.snapshot(command_id_text::uuid, false);\n IF submission IS NULL THEN RETURN NULL; END IF;\n RETURN jsonb_build_object('kind','submission','commandId',command_id_text::uuid,\n 'execution',NULL,'workflow',jsonb_build_object(\n 'runId',submission->'workflow'->'runId','status',submission->'workflow'->'status'));\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n PERFORM supacloud_commands.submit(request);\n RETURN supacloud_commands.status(jsonb_build_object('commandId',request->>'commandId'));\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_commands.status(request)\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;\n-- supacloud:sql-module:commands-public:end\n\n-- supacloud:sql-module:artifacts-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_artifacts;\nREVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (\n id uuid PRIMARY KEY,\n storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,\n bucket_id text NOT NULL,\n object_path text NOT NULL,\n object_version text NOT NULL,\n artifact_type text NOT NULL\n CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),\n size_bytes bigint NOT NULL CHECK (size_bytes >= 0),\n mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n retention_until timestamptz,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n CHECK (retention_until IS NULL OR retention_until >= created_at),\n UNIQUE (bucket_id, object_path, object_version)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx\n ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (\n parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n relation_type text NOT NULL\n CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),\n CHECK (parent_artifact_id <> child_artifact_id)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx\n ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()\nRETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.storage_object_id = OLD.id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';\n END IF;\n RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;\nCREATE TRIGGER supacloud_artifact_storage_fence\nBEFORE UPDATE OR DELETE ON storage.objects\nFOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(\n p_artifact_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'artifactId', artifact.id,\n 'bucketId', artifact.bucket_id,\n 'objectPath', artifact.object_path,\n 'objectVersion', artifact.object_version,\n 'artifactType', artifact.artifact_type,\n 'sha256', artifact.sha256,\n 'sizeBytes', artifact.size_bytes::text,\n 'mimeType', artifact.mime_type,\n 'metadata', artifact.metadata,\n 'retentionUntil', artifact.retention_until,\n 'createdBy', artifact.created_by,\n 'createdAt', artifact.created_at,\n 'idempotent', p_idempotent,\n 'parents', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'artifactId', edge.parent_artifact_id,\n 'relationType', edge.relation_type,\n 'metadata', edge.metadata,\n 'createdAt', edge.created_at\n ) ORDER BY edge.created_at, edge.parent_artifact_id)\n FROM supacloud_artifacts.lineage edge\n WHERE edge.child_artifact_id = artifact.id\n ), '[]'::jsonb)\n )\n FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.id = p_artifact_id\n$$;\n\nDROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);\nCREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n artifact_id uuid;\n size_bytes bigint;\n metadata jsonb;\n retention_until timestamptz;\n created_by uuid;\n normalized_bucket text;\n normalized_path text;\n normalized_type text;\n normalized_sha256 text;\n normalized_mime text;\n object_row storage.objects%ROWTYPE;\n existing supacloud_artifacts.artifacts%ROWTYPE;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n artifact_id := (request ->> 'artifactId')::uuid;\n size_bytes := (request ->> 'sizeBytes')::bigint;\n metadata := coalesce(request -> 'metadata', '{}'::jsonb);\n retention_until := (request ->> 'retentionUntil')::timestamptz;\n created_by := (request ->> 'createdBy')::uuid;\n normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');\n normalized_path := nullif(btrim(request ->> 'objectPath'), '');\n normalized_type := nullif(btrim(request ->> 'artifactType'), '');\n normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));\n normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));\n IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL\n OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'\n OR normalized_path ~ '(^|/)(.|..)(/|$)'\n OR normalized_type IS NULL\n OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'\n OR size_bytes IS NULL OR size_bytes < 0\n OR normalized_mime IS NULL OR char_length(normalized_mime) > 255\n OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'\n OR (retention_until IS NOT NULL AND retention_until < now()) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO object_row FROM storage.objects\n WHERE bucket_id = normalized_bucket AND name = normalized_path;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));\n SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;\n IF FOUND THEN\n IF existing.storage_object_id <> object_row.id\n OR existing.artifact_type <> normalized_type\n OR existing.sha256 <> normalized_sha256\n OR existing.size_bytes <> size_bytes\n OR existing.mime_type <> normalized_mime\n OR existing.metadata <> metadata\n OR existing.retention_until IS DISTINCT FROM retention_until\n OR existing.created_by IS DISTINCT FROM created_by THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(artifact_id, true);\n END IF;\n\n INSERT INTO supacloud_artifacts.artifacts (\n id, storage_object_id, bucket_id, object_path, object_version,\n artifact_type, sha256, size_bytes, mime_type, metadata,\n retention_until, created_by\n ) VALUES (\n artifact_id, object_row.id, normalized_bucket, normalized_path,\n object_row.version::text, normalized_type, normalized_sha256,\n size_bytes, normalized_mime, metadata, retention_until, created_by\n );\n RETURN supacloud_artifacts.snapshot(artifact_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.link(\n p_parent_artifact_id uuid,\n p_child_artifact_id uuid,\n p_relation_type text,\n p_metadata jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_relation text := nullif(btrim(p_relation_type), '');\n inserted_count integer;\nBEGIN\n IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL\n OR p_parent_artifact_id = p_child_artifact_id\n OR normalized_relation IS NULL\n OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (\n WITH RECURSIVE descendants(artifact_id) AS (\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n WHERE edge.parent_artifact_id = p_child_artifact_id\n UNION\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n JOIN descendants current ON edge.parent_artifact_id = current.artifact_id\n )\n SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';\n END IF;\n INSERT INTO supacloud_artifacts.lineage (\n parent_artifact_id, child_artifact_id, relation_type, metadata\n ) VALUES (\n p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata\n ) ON CONFLICT DO NOTHING;\n GET DIAGNOSTICS inserted_count = ROW_COUNT;\n IF inserted_count = 0 AND NOT EXISTS (\n SELECT 1 FROM supacloud_artifacts.lineage\n WHERE parent_artifact_id = p_parent_artifact_id\n AND child_artifact_id = p_child_artifact_id\n AND relation_type = normalized_relation\n AND metadata = p_metadata\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)\nRETURNS jsonb\nLANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_artifacts.register(request)\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.link(\n (request ->> 'parentArtifactId')::uuid,\n (request ->> 'childArtifactId')::uuid,\n request ->> 'relationType',\n coalesce(request -> 'metadata', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;\n-- supacloud:sql-module:artifacts-public:end\n";
|
|
23
23
|
/**
|
|
24
24
|
* pg_cron emulation: the `cron.job` / `cron.job_run_details` tables and
|
|
25
25
|
* schedule/unschedule functions. This only records jobs; the in-process
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;GAIG;AACH,eAAO,MAAM,aAAa,uwRAyLzB,CAAA;AAED;gFACgF;AAChF,eAAO,MAAM,eAAe,uuWAkT3B,CAAA;AAED,eAAO,MAAM,QAAQ,2+nBACF,CAAA;AAEnB,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;GAIG;AACH,eAAO,MAAM,aAAa,uwRAyLzB,CAAA;AAED;gFACgF;AAChF,eAAO,MAAM,eAAe,uuWAkT3B,CAAA;AAED,eAAO,MAAM,QAAQ,2+nBACF,CAAA;AAEnB,eAAO,MAAM,aAAa,gqpEAwjDzB,CAAA;AAID;;;;GAIG;AACH,eAAO,MAAM,QAAQ,uyEA6DpB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,82GA0EnB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,ulBAqB1B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,m/FAoErB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/lite",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@electric-sql/pglite-pgmq": "0.0.8",
|
|
61
61
|
"@electric-sql/pglite-pgvector": "0.0.11",
|
|
62
62
|
"@electric-sql/pglite-postgis": "0.2.10",
|
|
63
|
-
"@supacloud/db": "0.20.
|
|
63
|
+
"@supacloud/db": "0.20.5",
|
|
64
64
|
"exact-mirror": "1.2.6",
|
|
65
65
|
"fast-xml-parser": "5.11.2",
|
|
66
66
|
"tar": "^7.5.22",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@supacloud/app": "0.36.0",
|
|
71
71
|
"@supacloud/contracts": "0.15.2",
|
|
72
|
-
"@supacloud/compiler": "0.
|
|
73
|
-
"@supacloud/elysia": "0.38.
|
|
72
|
+
"@supacloud/compiler": "0.55.0",
|
|
73
|
+
"@supacloud/elysia": "0.38.1",
|
|
74
74
|
"@supabase/supabase-js": "^2.118.0",
|
|
75
75
|
"@types/bun": "^1.4.4",
|
|
76
76
|
"effect": "4.0.5",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"typescript": "^7.0.2"
|
|
80
80
|
},
|
|
81
81
|
"overrides": {
|
|
82
|
-
"@supacloud/db": "0.20.
|
|
82
|
+
"@supacloud/db": "0.20.5",
|
|
83
83
|
"@supacloud/app": "0.36.0",
|
|
84
|
-
"@supacloud/compiler": "0.
|
|
85
|
-
"@supacloud/elysia": "0.38.
|
|
84
|
+
"@supacloud/compiler": "0.55.0",
|
|
85
|
+
"@supacloud/elysia": "0.38.1",
|
|
86
86
|
"@supacloud/contracts": "0.15.2",
|
|
87
87
|
"@supacloud/commands": "0.11.2",
|
|
88
88
|
"@supacloud/delivery": "0.24.0",
|