@supacloud/lite 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/cli.js +98 -13
- package/dist/index.js +98 -13
- package/dist/runtime/db/emulated.d.ts +1 -1
- package/dist/runtime/db/emulated.d.ts.map +1 -1
- package/dist/runtime/graphql.d.ts +10 -0
- package/dist/runtime/graphql.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.21.1...supacloud-lite-v0.22.0) (2026-10-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **platform:** close application platform P0/P1 capabilities ([dbaf3a6](https://github.com/vibeunion/supacloud/commit/dbaf3a6d01daa71455a8cde96e9e233743bfdfca))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **worker:** ship reviewed PGMQ job controls and concurrency repairs ([#1689](https://github.com/vibeunion/supacloud/issues/1689)) ([e283915](https://github.com/vibeunion/supacloud/commit/e283915e66e119b75ad1b909e18816270b5d01e3))
|
|
14
|
+
|
|
15
|
+
## [0.21.1](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.21.0...supacloud-lite-v0.21.1) (2026-10-06)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **deps,perf:** replace Bun native APIs and resolve audit vulnerabilities ([#1667](https://github.com/vibeunion/supacloud/issues/1667)) ([06a9199](https://github.com/vibeunion/supacloud/commit/06a91990c754ce586f2a2353f253dc50f4fa0f98))
|
|
21
|
+
|
|
3
22
|
## [0.21.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.20.0...supacloud-lite-v0.21.0) (2026-10-05)
|
|
4
23
|
|
|
5
24
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname as dirname8, join as join10, resolve as resolve6 } from "path";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.
|
|
11
|
+
version: "0.22.0",
|
|
12
12
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
@@ -44,7 +44,7 @@ var package_default = {
|
|
|
44
44
|
"build:standalone:release": "bun run scripts/build-standalone.ts linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64",
|
|
45
45
|
"build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
|
|
46
46
|
check: "bun run typecheck && bun run test && bun run build && bun run test:package && bun run build:standalone:host && bun run test:standalone",
|
|
47
|
-
clean:
|
|
47
|
+
clean: "bun -e 'import { $ } from \"bun\"; await $`rm -rf dist`'",
|
|
48
48
|
dev: "bun run src/cli.ts start",
|
|
49
49
|
start: "bun run src/cli.ts start",
|
|
50
50
|
test: "bun test --timeout 20000",
|
|
@@ -85,7 +85,8 @@ var package_default = {
|
|
|
85
85
|
"@supacloud/elysia": "file:../elysia",
|
|
86
86
|
"@supacloud/contracts": "file:../contracts",
|
|
87
87
|
"@supacloud/commands": "file:../commands",
|
|
88
|
-
"@supacloud/delivery": "file:../delivery"
|
|
88
|
+
"@supacloud/delivery": "file:../delivery",
|
|
89
|
+
"@graphql-tools/utils": "^12.0.3"
|
|
89
90
|
},
|
|
90
91
|
engines: {
|
|
91
92
|
bun: ">=1.4.2"
|
|
@@ -3939,6 +3940,21 @@ $pgmq_extension$;
|
|
|
3939
3940
|
|
|
3940
3941
|
CREATE SCHEMA IF NOT EXISTS pgmq_public;
|
|
3941
3942
|
GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3943
|
+
CREATE SCHEMA IF NOT EXISTS supacloud_queue;
|
|
3944
|
+
CREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (
|
|
3945
|
+
queue_name text NOT NULL,
|
|
3946
|
+
job_key text NOT NULL,
|
|
3947
|
+
msg_id bigint NOT NULL,
|
|
3948
|
+
message jsonb NOT NULL,
|
|
3949
|
+
sleep_seconds integer NOT NULL,
|
|
3950
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
3951
|
+
PRIMARY KEY (queue_name, job_key)
|
|
3952
|
+
);
|
|
3953
|
+
-- Upgrade early key-only installations without inventing an input identity.
|
|
3954
|
+
ALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;
|
|
3955
|
+
ALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;
|
|
3956
|
+
REVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;
|
|
3957
|
+
REVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;
|
|
3942
3958
|
|
|
3943
3959
|
CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
|
|
3944
3960
|
RETURNS text
|
|
@@ -4113,6 +4129,57 @@ SECURITY DEFINER
|
|
|
4113
4129
|
SET search_path = ''
|
|
4114
4130
|
AS $$ 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); $$;
|
|
4115
4131
|
|
|
4132
|
+
-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.
|
|
4133
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(
|
|
4134
|
+
queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0
|
|
4135
|
+
)
|
|
4136
|
+
RETURNS TABLE (msg_id text, created boolean)
|
|
4137
|
+
LANGUAGE plpgsql
|
|
4138
|
+
VOLATILE
|
|
4139
|
+
SECURITY DEFINER
|
|
4140
|
+
SET search_path = ''
|
|
4141
|
+
AS $$
|
|
4142
|
+
DECLARE
|
|
4143
|
+
normalized_queue text := pgmq_public.require_public_queue(queue_name);
|
|
4144
|
+
captured_message jsonb := pgmq_public.require_message(message);
|
|
4145
|
+
captured_delay integer := pgmq_public.require_seconds(sleep_seconds);
|
|
4146
|
+
inserted boolean;
|
|
4147
|
+
sent_id bigint;
|
|
4148
|
+
existing_id bigint;
|
|
4149
|
+
existing_message jsonb;
|
|
4150
|
+
existing_delay integer;
|
|
4151
|
+
BEGIN
|
|
4152
|
+
IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN
|
|
4153
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';
|
|
4154
|
+
END IF;
|
|
4155
|
+
INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)
|
|
4156
|
+
VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)
|
|
4157
|
+
ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;
|
|
4158
|
+
inserted := FOUND;
|
|
4159
|
+
IF NOT inserted THEN
|
|
4160
|
+
SELECT keys.msg_id, keys.message, keys.sleep_seconds
|
|
4161
|
+
INTO existing_id, existing_message, existing_delay
|
|
4162
|
+
FROM supacloud_queue.job_keys AS keys
|
|
4163
|
+
WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;
|
|
4164
|
+
IF existing_id IS NULL OR existing_id < 1 THEN
|
|
4165
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';
|
|
4166
|
+
END IF;
|
|
4167
|
+
IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN
|
|
4168
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';
|
|
4169
|
+
END IF;
|
|
4170
|
+
-- This is a durable enqueue receipt, not evidence that the message is still pending.
|
|
4171
|
+
RETURN QUERY SELECT existing_id::text, false;
|
|
4172
|
+
RETURN;
|
|
4173
|
+
END IF;
|
|
4174
|
+
SELECT receipt.msg_id INTO STRICT sent_id
|
|
4175
|
+
FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);
|
|
4176
|
+
PERFORM pgmq_public.require_message_id(sent_id);
|
|
4177
|
+
UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id
|
|
4178
|
+
WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;
|
|
4179
|
+
RETURN QUERY SELECT sent_id::text, true;
|
|
4180
|
+
END;
|
|
4181
|
+
$$;
|
|
4182
|
+
|
|
4116
4183
|
CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
|
|
4117
4184
|
RETURNS TABLE (
|
|
4118
4185
|
msg_id text, read_ct integer, enqueued_at timestamptz,
|
|
@@ -10724,24 +10791,40 @@ class GraphqlHandler {
|
|
|
10724
10791
|
schemas;
|
|
10725
10792
|
maxBytes;
|
|
10726
10793
|
timeoutMs;
|
|
10794
|
+
slowQueryThresholdMs;
|
|
10795
|
+
onRequest;
|
|
10727
10796
|
constructor(db, capability, options = {}, schemas = ["public"]) {
|
|
10728
10797
|
this.db = db;
|
|
10729
10798
|
this.capability = capability;
|
|
10730
10799
|
this.schemas = schemas;
|
|
10731
10800
|
this.maxBytes = positive(options.maxRequestBodyBytes ?? 1024 * 1024);
|
|
10732
10801
|
this.timeoutMs = positive(options.statementTimeoutMs ?? 30000);
|
|
10802
|
+
this.slowQueryThresholdMs = positive(options.slowQueryThresholdMs ?? 1000);
|
|
10803
|
+
this.onRequest = options.onRequest;
|
|
10733
10804
|
}
|
|
10734
10805
|
async handle(request, context) {
|
|
10806
|
+
const startedAt = performance.now();
|
|
10807
|
+
let operationName;
|
|
10808
|
+
const finish = (result) => {
|
|
10809
|
+
const durationMs = Math.round((performance.now() - startedAt) * 100) / 100;
|
|
10810
|
+
this.onRequest?.({
|
|
10811
|
+
...operationName === undefined ? {} : { operationName },
|
|
10812
|
+
status: result.status,
|
|
10813
|
+
durationMs,
|
|
10814
|
+
slow: durationMs >= this.slowQueryThresholdMs
|
|
10815
|
+
});
|
|
10816
|
+
return result;
|
|
10817
|
+
};
|
|
10735
10818
|
if (this.capability.status !== "supported") {
|
|
10736
|
-
return response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10819
|
+
return finish(response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10737
10820
|
errors: [{
|
|
10738
10821
|
message: "GraphQL requires the real pg_graphql extension and graphql.resolve.",
|
|
10739
10822
|
extensions: { code: this.capability.reason ?? "GRAPHQL_DISABLED" }
|
|
10740
10823
|
}]
|
|
10741
|
-
});
|
|
10824
|
+
}));
|
|
10742
10825
|
}
|
|
10743
10826
|
if (request.method !== "POST")
|
|
10744
|
-
return response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" });
|
|
10827
|
+
return finish(response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" }));
|
|
10745
10828
|
try {
|
|
10746
10829
|
if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
10747
10830
|
throw new ApiError(415, { message: "GraphQL requests require application/json" });
|
|
@@ -10750,6 +10833,7 @@ class GraphqlHandler {
|
|
|
10750
10833
|
if (!isObject(body) || typeof body.query !== "string" || !body.query.trim() || body.variables != null && !isObject(body.variables) || body.extensions != null && !isObject(body.extensions) || body.operationName != null && typeof body.operationName !== "string") {
|
|
10751
10834
|
throw new ApiError(400, { message: "Invalid GraphQL request" });
|
|
10752
10835
|
}
|
|
10836
|
+
operationName = body.operationName ?? undefined;
|
|
10753
10837
|
const result = await this.db.withContext(context, async (query) => {
|
|
10754
10838
|
await query(`select set_config('statement_timeout', $1, true)`, [String(this.timeoutMs)]);
|
|
10755
10839
|
await query(`select set_config('search_path', $1, true)`, [
|
|
@@ -10761,18 +10845,18 @@ class GraphqlHandler {
|
|
|
10761
10845
|
if (result.rows.length !== 1 || !isRecord(row) || !isGraphqlEnvelope(row["result"])) {
|
|
10762
10846
|
throw new Error("Invalid GraphQL resolver response");
|
|
10763
10847
|
}
|
|
10764
|
-
return response(200, row["result"]);
|
|
10848
|
+
return finish(response(200, row["result"]));
|
|
10765
10849
|
} catch (error) {
|
|
10766
10850
|
if (error instanceof ApiError)
|
|
10767
|
-
return response(error.status, { errors: [{ message: error.message }] });
|
|
10851
|
+
return finish(response(error.status, { errors: [{ message: error.message }] }));
|
|
10768
10852
|
if (error instanceof SyntaxError)
|
|
10769
|
-
return response(400, { errors: [{ message: "Invalid JSON request" }] });
|
|
10853
|
+
return finish(response(400, { errors: [{ message: "Invalid JSON request" }] }));
|
|
10770
10854
|
const code = errorProperty(error, "errno") ?? errorProperty(error, "code");
|
|
10771
10855
|
const status = code === "42501" ? 403 : code === "57014" ? 504 : 500;
|
|
10772
|
-
return response(status, { errors: [{
|
|
10856
|
+
return finish(response(status, { errors: [{
|
|
10773
10857
|
message: "GraphQL execution failed",
|
|
10774
10858
|
extensions: { code: status === 403 ? "GRAPHQL_ACCESS_DENIED" : status === 504 ? "GRAPHQL_TIMEOUT" : "GRAPHQL_EXECUTION_FAILED" }
|
|
10775
|
-
}] });
|
|
10859
|
+
}] }));
|
|
10776
10860
|
}
|
|
10777
10861
|
}
|
|
10778
10862
|
}
|
|
@@ -13882,11 +13966,12 @@ function readLite(root, mode) {
|
|
|
13882
13966
|
graphql: {
|
|
13883
13967
|
enabled: getBool(graphql, "enabled"),
|
|
13884
13968
|
maxRequestBodyBytes: getInt(graphql, "max_request_body_bytes"),
|
|
13885
|
-
statementTimeoutMs: getInt(graphql, "statement_timeout_ms")
|
|
13969
|
+
statementTimeoutMs: getInt(graphql, "statement_timeout_ms"),
|
|
13970
|
+
slowQueryThresholdMs: getInt(graphql, "slow_query_threshold_ms")
|
|
13886
13971
|
},
|
|
13887
13972
|
identityModule: getString(tableAt(root, "lite.identity"), "module")
|
|
13888
13973
|
};
|
|
13889
|
-
for (const key of ["max_request_body_bytes", "statement_timeout_ms"]) {
|
|
13974
|
+
for (const key of ["max_request_body_bytes", "statement_timeout_ms", "slow_query_threshold_ms"]) {
|
|
13890
13975
|
const value = graphql?.values.get(key);
|
|
13891
13976
|
if (value !== undefined && (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0)) {
|
|
13892
13977
|
throw new Error(`invalid GraphQL option: ${key}`);
|
package/dist/index.js
CHANGED
|
@@ -83,7 +83,7 @@ function randomToken(bytes = 32) {
|
|
|
83
83
|
// package.json
|
|
84
84
|
var package_default = {
|
|
85
85
|
name: "@supacloud/lite",
|
|
86
|
-
version: "0.
|
|
86
|
+
version: "0.22.0",
|
|
87
87
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
88
88
|
type: "module",
|
|
89
89
|
license: "Apache-2.0",
|
|
@@ -119,7 +119,7 @@ var package_default = {
|
|
|
119
119
|
"build:standalone:release": "bun run scripts/build-standalone.ts linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64",
|
|
120
120
|
"build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
|
|
121
121
|
check: "bun run typecheck && bun run test && bun run build && bun run test:package && bun run build:standalone:host && bun run test:standalone",
|
|
122
|
-
clean:
|
|
122
|
+
clean: "bun -e 'import { $ } from \"bun\"; await $`rm -rf dist`'",
|
|
123
123
|
dev: "bun run src/cli.ts start",
|
|
124
124
|
start: "bun run src/cli.ts start",
|
|
125
125
|
test: "bun test --timeout 20000",
|
|
@@ -160,7 +160,8 @@ var package_default = {
|
|
|
160
160
|
"@supacloud/elysia": "file:../elysia",
|
|
161
161
|
"@supacloud/contracts": "file:../contracts",
|
|
162
162
|
"@supacloud/commands": "file:../commands",
|
|
163
|
-
"@supacloud/delivery": "file:../delivery"
|
|
163
|
+
"@supacloud/delivery": "file:../delivery",
|
|
164
|
+
"@graphql-tools/utils": "^12.0.3"
|
|
164
165
|
},
|
|
165
166
|
engines: {
|
|
166
167
|
bun: ">=1.4.2"
|
|
@@ -3942,6 +3943,21 @@ $pgmq_extension$;
|
|
|
3942
3943
|
|
|
3943
3944
|
CREATE SCHEMA IF NOT EXISTS pgmq_public;
|
|
3944
3945
|
GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3946
|
+
CREATE SCHEMA IF NOT EXISTS supacloud_queue;
|
|
3947
|
+
CREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (
|
|
3948
|
+
queue_name text NOT NULL,
|
|
3949
|
+
job_key text NOT NULL,
|
|
3950
|
+
msg_id bigint NOT NULL,
|
|
3951
|
+
message jsonb NOT NULL,
|
|
3952
|
+
sleep_seconds integer NOT NULL,
|
|
3953
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
3954
|
+
PRIMARY KEY (queue_name, job_key)
|
|
3955
|
+
);
|
|
3956
|
+
-- Upgrade early key-only installations without inventing an input identity.
|
|
3957
|
+
ALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;
|
|
3958
|
+
ALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;
|
|
3959
|
+
REVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;
|
|
3960
|
+
REVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;
|
|
3945
3961
|
|
|
3946
3962
|
CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
|
|
3947
3963
|
RETURNS text
|
|
@@ -4116,6 +4132,57 @@ SECURITY DEFINER
|
|
|
4116
4132
|
SET search_path = ''
|
|
4117
4133
|
AS $$ 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); $$;
|
|
4118
4134
|
|
|
4135
|
+
-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.
|
|
4136
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(
|
|
4137
|
+
queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0
|
|
4138
|
+
)
|
|
4139
|
+
RETURNS TABLE (msg_id text, created boolean)
|
|
4140
|
+
LANGUAGE plpgsql
|
|
4141
|
+
VOLATILE
|
|
4142
|
+
SECURITY DEFINER
|
|
4143
|
+
SET search_path = ''
|
|
4144
|
+
AS $$
|
|
4145
|
+
DECLARE
|
|
4146
|
+
normalized_queue text := pgmq_public.require_public_queue(queue_name);
|
|
4147
|
+
captured_message jsonb := pgmq_public.require_message(message);
|
|
4148
|
+
captured_delay integer := pgmq_public.require_seconds(sleep_seconds);
|
|
4149
|
+
inserted boolean;
|
|
4150
|
+
sent_id bigint;
|
|
4151
|
+
existing_id bigint;
|
|
4152
|
+
existing_message jsonb;
|
|
4153
|
+
existing_delay integer;
|
|
4154
|
+
BEGIN
|
|
4155
|
+
IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN
|
|
4156
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';
|
|
4157
|
+
END IF;
|
|
4158
|
+
INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)
|
|
4159
|
+
VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)
|
|
4160
|
+
ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;
|
|
4161
|
+
inserted := FOUND;
|
|
4162
|
+
IF NOT inserted THEN
|
|
4163
|
+
SELECT keys.msg_id, keys.message, keys.sleep_seconds
|
|
4164
|
+
INTO existing_id, existing_message, existing_delay
|
|
4165
|
+
FROM supacloud_queue.job_keys AS keys
|
|
4166
|
+
WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;
|
|
4167
|
+
IF existing_id IS NULL OR existing_id < 1 THEN
|
|
4168
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';
|
|
4169
|
+
END IF;
|
|
4170
|
+
IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN
|
|
4171
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';
|
|
4172
|
+
END IF;
|
|
4173
|
+
-- This is a durable enqueue receipt, not evidence that the message is still pending.
|
|
4174
|
+
RETURN QUERY SELECT existing_id::text, false;
|
|
4175
|
+
RETURN;
|
|
4176
|
+
END IF;
|
|
4177
|
+
SELECT receipt.msg_id INTO STRICT sent_id
|
|
4178
|
+
FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);
|
|
4179
|
+
PERFORM pgmq_public.require_message_id(sent_id);
|
|
4180
|
+
UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id
|
|
4181
|
+
WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;
|
|
4182
|
+
RETURN QUERY SELECT sent_id::text, true;
|
|
4183
|
+
END;
|
|
4184
|
+
$$;
|
|
4185
|
+
|
|
4119
4186
|
CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
|
|
4120
4187
|
RETURNS TABLE (
|
|
4121
4188
|
msg_id text, read_ct integer, enqueued_at timestamptz,
|
|
@@ -10715,24 +10782,40 @@ class GraphqlHandler {
|
|
|
10715
10782
|
schemas;
|
|
10716
10783
|
maxBytes;
|
|
10717
10784
|
timeoutMs;
|
|
10785
|
+
slowQueryThresholdMs;
|
|
10786
|
+
onRequest;
|
|
10718
10787
|
constructor(db, capability, options = {}, schemas = ["public"]) {
|
|
10719
10788
|
this.db = db;
|
|
10720
10789
|
this.capability = capability;
|
|
10721
10790
|
this.schemas = schemas;
|
|
10722
10791
|
this.maxBytes = positive(options.maxRequestBodyBytes ?? 1024 * 1024);
|
|
10723
10792
|
this.timeoutMs = positive(options.statementTimeoutMs ?? 30000);
|
|
10793
|
+
this.slowQueryThresholdMs = positive(options.slowQueryThresholdMs ?? 1000);
|
|
10794
|
+
this.onRequest = options.onRequest;
|
|
10724
10795
|
}
|
|
10725
10796
|
async handle(request, context) {
|
|
10797
|
+
const startedAt = performance.now();
|
|
10798
|
+
let operationName;
|
|
10799
|
+
const finish = (result) => {
|
|
10800
|
+
const durationMs = Math.round((performance.now() - startedAt) * 100) / 100;
|
|
10801
|
+
this.onRequest?.({
|
|
10802
|
+
...operationName === undefined ? {} : { operationName },
|
|
10803
|
+
status: result.status,
|
|
10804
|
+
durationMs,
|
|
10805
|
+
slow: durationMs >= this.slowQueryThresholdMs
|
|
10806
|
+
});
|
|
10807
|
+
return result;
|
|
10808
|
+
};
|
|
10726
10809
|
if (this.capability.status !== "supported") {
|
|
10727
|
-
return response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10810
|
+
return finish(response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10728
10811
|
errors: [{
|
|
10729
10812
|
message: "GraphQL requires the real pg_graphql extension and graphql.resolve.",
|
|
10730
10813
|
extensions: { code: this.capability.reason ?? "GRAPHQL_DISABLED" }
|
|
10731
10814
|
}]
|
|
10732
|
-
});
|
|
10815
|
+
}));
|
|
10733
10816
|
}
|
|
10734
10817
|
if (request.method !== "POST")
|
|
10735
|
-
return response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" });
|
|
10818
|
+
return finish(response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" }));
|
|
10736
10819
|
try {
|
|
10737
10820
|
if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
10738
10821
|
throw new ApiError(415, { message: "GraphQL requests require application/json" });
|
|
@@ -10741,6 +10824,7 @@ class GraphqlHandler {
|
|
|
10741
10824
|
if (!isObject(body) || typeof body.query !== "string" || !body.query.trim() || body.variables != null && !isObject(body.variables) || body.extensions != null && !isObject(body.extensions) || body.operationName != null && typeof body.operationName !== "string") {
|
|
10742
10825
|
throw new ApiError(400, { message: "Invalid GraphQL request" });
|
|
10743
10826
|
}
|
|
10827
|
+
operationName = body.operationName ?? undefined;
|
|
10744
10828
|
const result = await this.db.withContext(context, async (query) => {
|
|
10745
10829
|
await query(`select set_config('statement_timeout', $1, true)`, [String(this.timeoutMs)]);
|
|
10746
10830
|
await query(`select set_config('search_path', $1, true)`, [
|
|
@@ -10752,18 +10836,18 @@ class GraphqlHandler {
|
|
|
10752
10836
|
if (result.rows.length !== 1 || !isRecord(row) || !isGraphqlEnvelope(row["result"])) {
|
|
10753
10837
|
throw new Error("Invalid GraphQL resolver response");
|
|
10754
10838
|
}
|
|
10755
|
-
return response(200, row["result"]);
|
|
10839
|
+
return finish(response(200, row["result"]));
|
|
10756
10840
|
} catch (error) {
|
|
10757
10841
|
if (error instanceof ApiError)
|
|
10758
|
-
return response(error.status, { errors: [{ message: error.message }] });
|
|
10842
|
+
return finish(response(error.status, { errors: [{ message: error.message }] }));
|
|
10759
10843
|
if (error instanceof SyntaxError)
|
|
10760
|
-
return response(400, { errors: [{ message: "Invalid JSON request" }] });
|
|
10844
|
+
return finish(response(400, { errors: [{ message: "Invalid JSON request" }] }));
|
|
10761
10845
|
const code = errorProperty(error, "errno") ?? errorProperty(error, "code");
|
|
10762
10846
|
const status = code === "42501" ? 403 : code === "57014" ? 504 : 500;
|
|
10763
|
-
return response(status, { errors: [{
|
|
10847
|
+
return finish(response(status, { errors: [{
|
|
10764
10848
|
message: "GraphQL execution failed",
|
|
10765
10849
|
extensions: { code: status === 403 ? "GRAPHQL_ACCESS_DENIED" : status === 504 ? "GRAPHQL_TIMEOUT" : "GRAPHQL_EXECUTION_FAILED" }
|
|
10766
|
-
}] });
|
|
10850
|
+
}] }));
|
|
10767
10851
|
}
|
|
10768
10852
|
}
|
|
10769
10853
|
}
|
|
@@ -13132,11 +13216,12 @@ function readLite(root, mode) {
|
|
|
13132
13216
|
graphql: {
|
|
13133
13217
|
enabled: getBool(graphql, "enabled"),
|
|
13134
13218
|
maxRequestBodyBytes: getInt(graphql, "max_request_body_bytes"),
|
|
13135
|
-
statementTimeoutMs: getInt(graphql, "statement_timeout_ms")
|
|
13219
|
+
statementTimeoutMs: getInt(graphql, "statement_timeout_ms"),
|
|
13220
|
+
slowQueryThresholdMs: getInt(graphql, "slow_query_threshold_ms")
|
|
13136
13221
|
},
|
|
13137
13222
|
identityModule: getString(tableAt(root, "lite.identity"), "module")
|
|
13138
13223
|
};
|
|
13139
|
-
for (const key of ["max_request_body_bytes", "statement_timeout_ms"]) {
|
|
13224
|
+
for (const key of ["max_request_body_bytes", "statement_timeout_ms", "slow_query_threshold_ms"]) {
|
|
13140
13225
|
const value = graphql?.values.get(key);
|
|
13141
13226
|
if (value !== undefined && (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0)) {
|
|
13142
13227
|
throw new Error(`invalid GraphQL option: ${key}`);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `a_<name>` tables plus create/send/read/pop/delete/archive/etc functions,
|
|
14
14
|
* matching pgmq's signatures. No C extension, no background worker needed.
|
|
15
15
|
*/
|
|
16
|
-
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-- 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;\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\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";
|
|
16
|
+
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-- 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";
|
|
17
17
|
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";
|
|
18
18
|
/**
|
|
19
19
|
* pg_cron emulation: the `cron.job` / `cron.job_run_details` tables and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;GAIG;AACH,eAAO,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;GAIG;AACH,eAAO,MAAM,QAAQ,u+nBA0epB,CAAA;AAED,eAAO,MAAM,aAAa,4mgEAwjDzB,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"}
|
|
@@ -11,6 +11,14 @@ export interface GraphqlOptions {
|
|
|
11
11
|
enabled?: boolean;
|
|
12
12
|
maxRequestBodyBytes?: number;
|
|
13
13
|
statementTimeoutMs?: number;
|
|
14
|
+
slowQueryThresholdMs?: number;
|
|
15
|
+
onRequest?: (metric: GraphqlRequestMetric) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface GraphqlRequestMetric {
|
|
18
|
+
operationName?: string;
|
|
19
|
+
status: number;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
slow: boolean;
|
|
14
22
|
}
|
|
15
23
|
type GraphqlProbe = {
|
|
16
24
|
query(sql: string): Promise<{
|
|
@@ -24,6 +32,8 @@ export declare class GraphqlHandler {
|
|
|
24
32
|
private schemas;
|
|
25
33
|
readonly maxBytes: number;
|
|
26
34
|
readonly timeoutMs: number;
|
|
35
|
+
readonly slowQueryThresholdMs: number;
|
|
36
|
+
private readonly onRequest?;
|
|
27
37
|
constructor(db: Database, capability: GraphqlCapability, options?: GraphqlOptions, schemas?: string[]);
|
|
28
38
|
handle(request: Request, context: RequestContext): Promise<Response>;
|
|
29
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/runtime/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAG1D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,GAAG,aAAa,GAAG,UAAU,GAAG,YAAY,CAAA;IAC/D,SAAS,EAAE,YAAY,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/runtime/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAG1D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,GAAG,aAAa,GAAG,UAAU,GAAG,YAAY,CAAA;IAC/D,SAAS,EAAE,YAAY,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAA;CACnD;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,KAAK,YAAY,GAAG;IAAE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,CAAA;AAExE,wBAAsB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoCrF;AAED,qBAAa,cAAc;IAMb,OAAO,CAAC,EAAE;IAAY,OAAO,CAAC,UAAU;IAAmD,OAAO,CAAC,OAAO;IALtH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAwC;gBAE/C,EAAE,EAAE,QAAQ,EAAU,UAAU,EAAE,iBAAiB,EAAE,OAAO,GAAE,cAAmB,EAAU,OAAO,WAAa;IAO7H,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;CA0D3E"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { NetService, type NetDelivery } from './net/service.js';
|
|
|
30
30
|
export { RetentionService, type RetentionConfig } from './retention/service.js';
|
|
31
31
|
export { snapshotSchema, diffSchemas, type SchemaSnapshot } from './db/schema-diff.js';
|
|
32
32
|
export { inspectDb, type TableInfo } from './db/inspect.js';
|
|
33
|
+
export type { GraphqlRequestMetric } from './graphql.js';
|
|
33
34
|
/**
|
|
34
35
|
* A running SupaCloud Lite backend. The one field a consumer always needs is
|
|
35
36
|
* {@link SupaCloudLiteBackend.fetch}; the rest expose the underlying services for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAA8B,MAAM,wBAAwB,CAAA;AAGrF,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAIrD,OAAO,EAAE,eAAe,EAAwB,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,aAAa,EAAU,aAAa,EAA6B,MAAM,YAAY,CAAA;AAEjG,OAAO,EAAkC,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAKrF,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACxD,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACjG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAA8B,MAAM,wBAAwB,CAAA;AAGrF,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAIrD,OAAO,EAAE,eAAe,EAAwB,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,aAAa,EAAU,aAAa,EAA6B,MAAM,YAAY,CAAA;AAEjG,OAAO,EAAkC,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAKrF,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACxD,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACjG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3D,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAExD;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,iBAAiB,CAAA;IAC1B,oGAAoG;IACpG,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,mFAAmF;IACnF,EAAE,EAAE,QAAQ,CAAA;IACZ,6EAA6E;IAC7E,QAAQ,EAAE,cAAc,CAAA;IACxB,6EAA6E;IAC7E,SAAS,EAAE,gBAAgB,CAAA;IAC3B,qEAAqE;IACrE,QAAQ,EAAE,eAAe,CAAA;IACzB,mCAAmC;IACnC,IAAI,EAAE,WAAW,CAAA;IACjB,wDAAwD;IACxD,GAAG,EAAE,UAAU,CAAA;IACf,6EAA6E;IAC7E,SAAS,EAAE,gBAAgB,CAAA;IAC3B,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAA;IACf,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,IAAI,EAAE,SAAS,CAAA;IACf,4FAA4F;IAC5F,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,gHAAgH;IAChH,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,8CAA8C;IAC9C,OAAO,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC7E,gGAAgG;IAChG,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkZ7F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"build:standalone:release": "bun run scripts/build-standalone.ts linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64",
|
|
37
37
|
"build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
|
|
38
38
|
"check": "bun run typecheck && bun run test && bun run build && bun run test:package && bun run build:standalone:host && bun run test:standalone",
|
|
39
|
-
"clean": "bun -e
|
|
39
|
+
"clean": "bun -e 'import { $ } from \"bun\"; await $`rm -rf dist`'",
|
|
40
40
|
"dev": "bun run src/cli.ts start",
|
|
41
41
|
"start": "bun run src/cli.ts start",
|
|
42
42
|
"test": "bun test --timeout 20000",
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"@supacloud/elysia": "file:../elysia",
|
|
78
78
|
"@supacloud/contracts": "file:../contracts",
|
|
79
79
|
"@supacloud/commands": "file:../commands",
|
|
80
|
-
"@supacloud/delivery": "file:../delivery"
|
|
80
|
+
"@supacloud/delivery": "file:../delivery",
|
|
81
|
+
"@graphql-tools/utils": "^12.0.3"
|
|
81
82
|
},
|
|
82
83
|
"engines": {
|
|
83
84
|
"bun": ">=1.4.2"
|