From a4a583f4e39a814b73c2b2eefb20bc82436d0e2b Mon Sep 17 00:00:00 2001 From: sqwishy Date: Wed, 14 Sep 2022 11:46:57 -0700 Subject: [PATCH] feat(backend): flow suspend resume (#522) Flow observes `suspend` setting and will wait for resume messages sent for the job before continuing to the next step in a flow. Adds endpoints under workspaces at `/jobs//` to either cancel or resume the job with a payload. For POST requests to the endpoint, payload is a JSON document. For GET requests to the endpoints, the payload is a base64url encoded JSON document as the value of the payload query parameter. --- backend/Cargo.lock | 1 + backend/Cargo.toml | 1 + .../20220902211654_job-resume.down.sql | 5 + .../20220902211654_job-resume.up.sql | 16 + backend/openapi.yaml | 62 + backend/sqlx-data.json | 3394 +++++++++-------- backend/src/error.rs | 10 + backend/src/fixtures/base.sql | 23 +- backend/src/flows.rs | 7 + backend/src/jobs.rs | 232 +- backend/src/lib.rs | 1 + backend/src/worker.rs | 469 ++- backend/src/worker_flow.rs | 177 +- openflow.openapi.yaml | 6 +- 14 files changed, 2658 insertions(+), 1746 deletions(-) create mode 100644 backend/migrations/20220902211654_job-resume.down.sql create mode 100644 backend/migrations/20220902211654_job-resume.up.sql diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bc5b523f39..1e7bac4bbf 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4481,6 +4481,7 @@ dependencies = [ "async-oauth2", "async-recursion", "axum", + "base64 0.11.0", "chrono", "console-subscriber", "cron", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 2f653d5172..7023dab3e1 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -58,6 +58,7 @@ async-recursion = "^1" swc_common = "^0" swc_ecma_parser = "^0" swc_ecma_ast = "^0" +base64 = "^0" unicode-general-category = "^0" sqlx = { version = "^0", features = ["macros", "offline", "migrate", "uuid", "json", "chrono", "postgres", "runtime-tokio-rustls"]} diff --git a/backend/migrations/20220902211654_job-resume.down.sql b/backend/migrations/20220902211654_job-resume.down.sql new file mode 100644 index 0000000000..2d43a54e1d --- /dev/null +++ b/backend/migrations/20220902211654_job-resume.down.sql @@ -0,0 +1,5 @@ +DROP TABLE resume_job; + +ALTER TABLE queue +DROP COLUMN suspend, +DROP COLUMN suspend_until; diff --git a/backend/migrations/20220902211654_job-resume.up.sql b/backend/migrations/20220902211654_job-resume.up.sql new file mode 100644 index 0000000000..b0a9fe77fa --- /dev/null +++ b/backend/migrations/20220902211654_job-resume.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE resume_job ( + id uuid NOT NULL, + job uuid NOT NULL, + flow uuid NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + value JSONB NOT NULL DEFAULT 'null'::jsonb + CHECK (length(value::text) < 10 * 1024), + is_cancel boolean NOT NULL default false, + + PRIMARY KEY (id), + FOREIGN KEY (flow) REFERENCES queue(id) ON DELETE CASCADE +); + +ALTER TABLE queue + ADD COLUMN suspend INTEGER NOT NULL DEFAULT 0, + ADD COLUMN suspend_until TIMESTAMPTZ; diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 6c2a43da9a..5a3a7bddb6 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -2551,6 +2551,68 @@ paths: schema: type: string + /w/{workspace}/jobs/resume/{id}: + get: + summary: resume a job for a suspended flow + operationId: resumeSuspendedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + - name: payload + in: query + schema: + type: object + responses: + "201" + post: + summary: resume a job for a suspended flow + operationId: resumeSuspendedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + requestBody: + content: + application/json: + schema: + type: object + responses: + "201" + + /w/{workspace}/jobs/cancel/{id}: + get: + summary: cancel a job for a suspended flow + operationId: cancelSuspendedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + - name: payload + in: query + schema: + type: object + responses: + "201" + post: + summary: cancel a job for a suspended flow + operationId: cancelSuspendedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + requestBody: + content: + application/json: + schema: + type: object + responses: + "201" + /schedules/preview: post: summary: preview schedule diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 295d443d11..559652a115 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -1,15 +1,17 @@ { "db": "PostgreSQL", "02cf26c6e81ce21fa84fdc2a431663aa2db7efea3596d7eec984dd707b8ad010": { - "query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6) RETURNING expiration", "describe": { "columns": [ { - "ordinal": 0, "name": "expiration", + "ordinal": 0, "type_info": "Timestamptz" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Varchar", @@ -19,172 +21,170 @@ "Text", "Bool" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6) RETURNING expiration" }, "0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": { - "query": "DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1", "describe": { "columns": [ { - "ordinal": 0, "name": "?column?", + "ordinal": 0, "type_info": "Int4" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Uuid" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1" }, "04a15674cf66f2822085e65ca33ee42b8e7f3f9d63ffac0585a12368d0322252": { - "query": "SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "group_", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2" }, "04effcc6050250a02661323c880d493982dd1bfb63ca7373e035a98c268428e2": { - "query": "SELECT script_path FROM queue WHERE id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "script_path", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Uuid" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "SELECT script_path FROM queue WHERE id = $1" }, "062859f1d0e5cfba3115f4241115753b86a4ad239708851c998ff5620ebca5b8": { - "query": "UPDATE queue SET last_ping = now() WHERE id = $1", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Uuid" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE queue SET last_ping = now() WHERE id = $1" }, "07551a32c49da8c0693dd39c6a63b5b2a596ccc0e52e8918160604a5e133dd32": { - "query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int4", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2" }, "0784bb86a503f02b9ef247a2b83a82ddfa49632552b223a9f4536a449b0a1eb8": { - "query": "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)" }, "097a576938eac385ddc2f16a00ddc69c3ca54f5a66923291730980eeeea1f8c1": { - "query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2" }, "0a7212dd507ed8f7a311724185e39ecc1809abb208a681ad711614c27baadd83": { - "query": "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "flow_status", + "ordinal": 0, "type_info": "Jsonb" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Uuid", "Text" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2" }, "0a76ed47629cac693ba7f169a1229b62bd900bc007a63fbae3fa7374ba66df65": { - "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, $3)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Varchar", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, $3)" }, "0ae9160591ae00117d20a616cfe07e38f0c32953c7e881e916c389255190b72d": { - "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -192,92 +192,92 @@ "Varchar", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)" }, "0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": { - "query": "SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)" }, "0dd3fe3ddf9cb72760687d2ee0950afdcce2d54721bfe8dba008b15e4b581956": { - "query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int4", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2" }, "103e321fbaa847831682b5cba2fd94f12c508ddf372f9facd18e30d00afd1ea3": { - "query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token WHERE email = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "label", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "token_prefix", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "expiration", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "created_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "last_used_at", + "ordinal": 4, "type_info": "Timestamptz" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ true, null, true, false, false - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token WHERE email = $1" }, "11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": { - "query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -287,93 +287,86 @@ "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)" }, "11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": { - "query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3", "describe": { "columns": [ { - "ordinal": 0, "name": "logs", + "ordinal": 0, "type_info": "Text" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Int4", "Text", "Uuid" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3" }, "122090a0f89e5248a0a0f199ebd24582fdb302883aebd2da187ac0084e767ea3": { - "query": "SELECT set_config('session.pgroups', $1, true)", "describe": { "columns": [ { - "ordinal": 0, "name": "set_config", + "ordinal": 0, "type_info": "Text" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT set_config('session.pgroups', $1, true)" }, "1289e7278d2a289bfaa53f00e0b6dceb195df0fb43a8ac03bc8b35939fc941dd": { - "query": "SELECT * FROM workspace LIMIT $1 OFFSET $2", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "name", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "owner", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "domain", + "ordinal": 3, "type_info": "Varchar" }, { - "ordinal": 4, "name": "deleted", + "ordinal": 4, "type_info": "Bool" }, { - "ordinal": 5, "name": "premium", + "ordinal": 5, "type_info": "Bool" } ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, "nullable": [ false, false, @@ -381,13 +374,46 @@ true, false, false - ] - } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT * FROM workspace LIMIT $1 OFFSET $2" + }, + "13a10a404e892d6975d4913424880a75bfb2c6dfb5134c270c7c7225acb05db4": { + "describe": { + "columns": [ + { + "name": "value", + "ordinal": 0, + "type_info": "Jsonb" + }, + { + "name": "is_cancel", + "ordinal": 1, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "SELECT value, is_cancel FROM resume_job WHERE job = $1 ORDER BY created_at ASC" }, "15de975d9be141c9ed9647935a508492aabbbddbf986d5c5c0f0c415293c432d": { - "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), \n ($3, 'g/all/not_secret', $4, false, 'This item is not secret')", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -395,167 +421,182 @@ "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), \n ($3, 'g/all/not_secret', $4, false, 'This item is not secret')" }, "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597": { - "query": "SELECT * FROM workspace_settings WHERE workspace_id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "slack_team_id", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "slack_name", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "slack_command_script", + "ordinal": 3, "type_info": "Varchar" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, true, true, true - ] - } - }, - "1ad8677694aca94ee0e6da287d7cc028dcf673583a0e3e4fedd0e5d6766c5860": { - "query": "DELETE FROM usr WHERE email = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - } - }, - "1b31847d6187d6969deac5aa7b2feb169ef963449ac2d3ea06e1ed785f6d42e7": { - "query": "SELECT * from workspace_invite WHERE email = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "is_admin", - "type_info": "Bool" - } ], "parameters": { "Left": [ "Text" ] - }, + } + }, + "query": "SELECT * FROM workspace_settings WHERE workspace_id = $1" + }, + "1ad8677694aca94ee0e6da287d7cc028dcf673583a0e3e4fedd0e5d6766c5860": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM usr WHERE email = $1" + }, + "1b31847d6187d6969deac5aa7b2feb169ef963449ac2d3ea06e1ed785f6d42e7": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "is_admin", + "ordinal": 2, + "type_info": "Bool" + } + ], "nullable": [ false, false, false - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT * from workspace_invite WHERE email = $1" }, "1bf2ca894246bd716875635b2d0c294a1ce2ed21916097ea165df240f7421a1e": { - "query": "UPDATE queue SET logs = $1 WHERE id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Uuid" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE queue SET logs = $1 WHERE id = $2" + }, + "1f93b533fa6fee0db4340445da3fac8e6773bc1db1f88cd60fd3c1e8c9781eb0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int4", + "Uuid" + ] + } + }, + "query": "UPDATE queue SET suspend = $1 WHERE id = $2" }, "21cd7cbab7799baf5c381427d9b373c0bb144715eddfe54e3b01f6049d7966a2": { - "query": "SELECT workspace.id, workspace.name, usr.username\n FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "name", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "username", + "ordinal": 2, "type_info": "Varchar" } ], + "nullable": [ + false, + false, + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false, - false, - false - ] - } + } + }, + "query": "SELECT workspace.id, workspace.name, usr.username\n FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false" }, "230d58732a08164268ca10d248a93cced646632a76864b693ed2325d85b36c45": { - "query": "SELECT canceled FROM queue WHERE id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "canceled", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Uuid" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT canceled FROM queue WHERE id = $1" }, "2420cb110a116dfbc6b8658a6d2d35db60c6aadd157488cb72eb9116ae7e9f54": { - "query": "INSERT INTO queue\n (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Uuid" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Varchar", @@ -570,7 +611,6 @@ "Jsonb", { "Custom": { - "name": "job_kind", "kind": { "Enum": [ "script", @@ -580,7 +620,8 @@ "flowpreview", "script_hub" ] - } + }, + "name": "job_kind" } }, "Varchar", @@ -589,65 +630,63 @@ "Bool", { "Custom": { - "name": "script_lang", "kind": { "Enum": [ "python3", "deno" ] - } + }, + "name": "script_lang" } } ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "INSERT INTO queue\n (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id" }, "255aafff962738317f3227ae4eb871830d89b4c12c73d8dbabe6836da124e54d": { - "query": "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", "describe": { "columns": [ { - "ordinal": 0, "name": "path", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" }, "2685d46490744f3f98718ca79a9a8f7c14628e09483b90831d151f99f561a983": { - "query": "SELECT now()", "describe": { "columns": [ { - "ordinal": 0, "name": "now", + "ordinal": 0, "type_info": "Timestamptz" } ], - "parameters": { - "Left": [] - }, "nullable": [ null - ] - } + ], + "parameters": { + "Left": [] + } + }, + "query": "SELECT now()" }, "27eb5f99dc9289670673fb999ba9e67abccba212b506c30ba47e6c4fef6d53c4": { - "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -657,105 +696,105 @@ "Varchar", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)" }, "28c042adef65c3055edc324fbbd2f267285d3566cbec58404983323d410ace27": { - "query": "SELECT super_admin FROM password WHERE email = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "super_admin", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT super_admin FROM password WHERE email = $1" }, "28c1afcce0446817543fc47dde29b1137b2550bac4a2b6e81c72c74a84bb84fb": { - "query": "UPDATE workspace_settings SET slack_command_script = $1 WHERE workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE workspace_settings SET slack_command_script = $1 WHERE workspace_id = $2" }, "2a4be8334db7d39f3d954193a8b0169cc4a4a07e081d2fa61d8764879d6a8ff5": { - "query": "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2" }, "2bf44d998d7acd17ec6d98f81395f8bdac49f58880fbbb9350bf0142cd2efdc7": { - "query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3" }, "2e11e3ef361c41e6e055dd4805cb9d5e45eaa486e35fc5f01dae311189d6800f": { - "query": "SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", "describe": { "columns": [ { - "ordinal": 0, "name": "language: ScriptLang", + "ordinal": 0, "type_info": { "Custom": { - "name": "script_lang", "kind": { "Enum": [ "python3", "deno" ] - } + }, + "name": "script_lang" } } } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" }, "2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755": { - "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -766,82 +805,91 @@ "Int4", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)" + }, + "30d85ec4bfa7e81d2803ea9c0b638a01257ae2e9138dd82df526d28d4467df26": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Jsonb", + "Bool" + ] + } + }, + "query": "\n INSERT INTO resume_job\n (id, job, flow, value, is_cancel)\n VALUES ($1, $2, $3, $4, $5)\n " }, "355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": { - "query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)" }, "3d363466d79075df3f74f946eff43ca89faefca3bcdf2c533425ca3868b0369a": { - "query": "SELECT * FROM usr where username = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "username", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "email", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "is_admin", + "ordinal": 3, "type_info": "Bool" }, { - "ordinal": 4, "name": "created_at", + "ordinal": 4, "type_info": "Timestamptz" }, { - "ordinal": 5, "name": "operator", + "ordinal": 5, "type_info": "Bool" }, { - "ordinal": 6, "name": "disabled", + "ordinal": 6, "type_info": "Bool" }, { - "ordinal": 7, "name": "role", + "ordinal": 7, "type_info": "Varchar" } ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, "nullable": [ false, false, @@ -851,81 +899,88 @@ false, false, true - ] - } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT * FROM usr where username = $1 AND workspace_id = $2" }, "3fabb3857c9cf2d057814b54ee54a95d01b6a7d9e89bea239b832a9d70f0044b": { - "query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false" }, "453501fbd61efd26647baf9b6ef702ce0bc2e920914e9f08fe5f2a5f4ab03f02": { - "query": "SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = $3", "describe": { "columns": [ { - "ordinal": 0, "name": "logs", + "ordinal": 0, "type_info": "Text" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Int4", "Text", "Uuid" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = $3" }, "486f181a9ced2bdc7c8d93da22c9d3e229ef106174bff2c472dbe82622f382b6": { - "query": "UPDATE queue SET logs = concat(logs, $1::text) WHERE id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Uuid" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE queue SET logs = concat(logs, $1::text) WHERE id = $2" }, "4ad5fa2f08236507aad911a95697e84fc0c3a274ba0e928da28c4d146cf8f1a8": { - "query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "is_admin", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2" }, "4e8ff055a80cc2e6b7fd6653c0c8e9a0e4b223a4bdeab0cf43dc79ce10f635b9": { - "query": "INSERT INTO workspace\n (id, name, owner, domain)\n VALUES ($1, $2, $3, $4)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -933,70 +988,82 @@ "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO workspace\n (id, name, owner, domain)\n VALUES ($1, $2, $3, $4)" }, "4eb6b80c410e00e8a72eced6c25cdd3ed941e7a46a0619173f272eec7f28a3c1": { - "query": "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "path", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "edited_by", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "edited_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "schedule", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "offset_", + "ordinal": 5, "type_info": "Int4" }, { - "ordinal": 6, "name": "enabled", + "ordinal": 6, "type_info": "Bool" }, { - "ordinal": 7, "name": "script_path", + "ordinal": 7, "type_info": "Varchar" }, { - "ordinal": 8, "name": "args", + "ordinal": 8, "type_info": "Jsonb" }, { - "ordinal": 9, "name": "extra_perms", + "ordinal": 9, "type_info": "Jsonb" }, { - "ordinal": 10, "name": "is_flow", + "ordinal": 10, "type_info": "Bool" } ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], "parameters": { "Left": [ "Varchar", @@ -1006,26 +1073,14 @@ "Text", "Text" ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false - ] - } + } + }, + "query": "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *" }, "5061c0d054bf4f028e7fe51a8f9389024c6ae4492755cadac0f7167e5300bda0": { - "query": "INSERT INTO resource_type\n (workspace_id, name, schema, description)\n VALUES ($1, $2, $3, $4)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1033,34 +1088,34 @@ "Jsonb", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO resource_type\n (workspace_id, name, schema, description)\n VALUES ($1, $2, $3, $4)" }, "50d1d62e1a0044168ec485c7f69bfb88ad4ecf200b33cd99f92da969628fb9f4": { - "query": "SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'", "describe": { "columns": [ { - "ordinal": 0, "name": "key", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'" }, "512390fc288b9f61bf5bb80ade168b17f09a6a52068a67edcf3f1f268bd53372": { - "query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1071,65 +1126,65 @@ "Varchar", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json)" }, "514c5feb8a29a2a6b577553d8577a46bcbb196c62d046a0568c573ce96ff3c43": { - "query": "DELETE FROM usr_to_group WHERE group_ = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM usr_to_group WHERE group_ = $1 AND workspace_id = $2" }, "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e": { - "query": "SELECT * FROM workspace_settings WHERE slack_team_id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "slack_team_id", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "slack_name", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "slack_command_script", + "ordinal": 3, "type_info": "Varchar" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, true, true, true - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT * FROM workspace_settings WHERE slack_team_id = $1" }, "54756c6c39888feb2206b056df1c84c3bb44adc490309954359845c06b6e607c": { - "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1138,87 +1193,119 @@ "Timestamptz", "Bool" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)" }, "58efbf34ba014b4853ef20ae400929b57c1d9de4262189274badf100d29e0649": { - "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "name", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "schema", + "ordinal": 2, "type_info": "Jsonb" }, { - "ordinal": 3, "name": "description", + "ordinal": 3, "type_info": "Text" } ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, "nullable": [ false, false, true, true - ] - } - }, - "5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00": { - "query": "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "email", - "type_info": "Varchar" - } ], "parameters": { "Left": [ "Text", "Text" ] - }, + } + }, + "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" + }, + "5a8eeff8ac0f52408720f8204bae085045cc4313bb31ab57442b093182633101": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "flow_status", + "ordinal": 1, + "type_info": "Jsonb" + }, + { + "name": "suspend", + "ordinal": 2, + "type_info": "Int4" + } + ], + "nullable": [ + false, + true, + false + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT id, flow_status, suspend\n FROM queue\n WHERE id = ( SELECT parent_job FROM queue WHERE id = $1\n UNION\n SELECT parent_job from completed_job WHERE id = $1 )\n FOR UPDATE\n " + }, + "5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00": { + "describe": { + "columns": [ + { + "name": "email", + "ordinal": 0, + "type_info": "Varchar" + } + ], "nullable": [ false - ] - } - }, - "5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": { - "query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2", - "describe": { - "columns": [], + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2" }, - "5fee34ce48f01bb55707227548ea4d1e77ba75abfae412a902b1d8d6a1963725": { - "query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14)", + "5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": { "describe": { "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2" + }, + "5fee34ce48f01bb55707227548ea4d1e77ba75abfae412a902b1d8d6a1963725": { + "describe": { + "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1235,314 +1322,317 @@ "Text", { "Custom": { - "name": "script_lang", "kind": { "Enum": [ "python3", "deno" ] - } + }, + "name": "script_lang" } }, { "Custom": { - "name": "script_kind", "kind": { "Enum": [ "script", "trigger", - "failure" + "failure", + "command" ] - } + }, + "name": "script_kind" } } ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14)" }, "6199e8be5cb13db71108e555ea20f0b76dc38476670f9fc0667b057d2766d42e": { - "query": "SELECT set_config('session.groups', $1, true)", "describe": { "columns": [ { - "ordinal": 0, "name": "set_config", + "ordinal": 0, "type_info": "Text" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT set_config('session.groups', $1, true)" }, "61e9662fe42506131222412ab3de48cf6485dea10aa3a2f97c0fd6322a0cb17f": { - "query": "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2" }, "62432110a09e68593ac52b3174c8bfa8736d5c7c1d8c7d6bed68f0dd9e06db7b": { - "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)" }, "63c4b9320681fac84ea92c25c0f6da5c9ac154dfccf575cea8145692246205c4": { - "query": "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", "describe": { "columns": [ { - "ordinal": 0, "name": "name", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name" }, "63f330e98051ae9d0cf5617a553f769cc98b05fdc8643839b504023b26f38aab": { - "query": "UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2" }, "64d2318064711c2cfbaf7c5c2b02d92cc98ac8e33eb560b829c462c3159115eb": { - "query": "INSERT INTO workspace_key\n (workspace_id, kind, key)\n VALUES ($1, 'cloud', $2)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO workspace_key\n (workspace_id, kind, key)\n VALUES ($1, 'cloud', $2)" }, "6c63bbcb45d3f51eccaea52ec862700e1f1c2426d823abd951e1eea4fd9b85aa": { - "query": "UPDATE script SET lock_error_logs = $1 WHERE hash = $2 AND workspace_id = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Int8", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE script SET lock_error_logs = $1 WHERE hash = $2 AND workspace_id = $3" }, "6c7186de56bcd9983a64de0c01a733e818ebc30af2377158c8a92ec66c06464c": { - "query": "UPDATE password SET super_admin = $1 WHERE email = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Bool", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE password SET super_admin = $1 WHERE email = $2" }, "6c90d5ea2a09b47b81fdab465062c25f8768b220e53bf469550a3a3697ab756a": { - "query": "SELECT set_config('session.user', $1, true)", "describe": { "columns": [ { - "ordinal": 0, "name": "set_config", + "ordinal": 0, "type_info": "Text" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ null - ] - } - }, - "6eacfedfc1ab2431c318996d2ff480a65d55fc43d8fa95aa4e2e8430722dc82d": { - "query": "SELECT email FROM usr WHERE username = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "email", - "type_info": "Varchar" - } ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT set_config('session.user', $1, true)" }, - "6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": { - "query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2", + "6eacfedfc1ab2431c318996d2ff480a65d55fc43d8fa95aa4e2e8430722dc82d": { "describe": { "columns": [ { + "name": "email", "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT email FROM usr WHERE username = $1" + }, + "6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": { + "describe": { + "columns": [ + { "name": "worker", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "worker_instance", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "ping_at", + "ordinal": 2, "type_info": "Timestamptz" }, { - "ordinal": 3, "name": "started_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "ip", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "jobs_executed", + "ordinal": 5, "type_info": "Int4" } ], + "nullable": [ + false, + false, + false, + false, + false, + false + ], "parameters": { "Left": [ "Int8", "Int8" ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - } + } + }, + "query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2" }, "701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": { - "query": "UPDATE workspace SET deleted = true WHERE id = $1", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE workspace SET deleted = true WHERE id = $1" }, "70e6ca3b5ad81f70376d31c75ba5f2b5dd94dd2f7f4cacd5a64029ccc3315d6c": { - "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, "nullable": [ null - ] - } - }, - "765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": { - "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "is_admin", - "type_info": "Bool" - } ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)" }, - "765d7a0562fec2a76355856df1b69a4536af25f0b5589b5d3828a75713d0331d": { - "query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 AND schedule_path IS NULL AND workspace_id = $4RETURNING id", + "765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": { "describe": { "columns": [ { + "name": "is_admin", "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin" + }, + "765d7a0562fec2a76355856df1b69a4536af25f0b5589b5d3828a75713d0331d": { + "describe": { + "columns": [ + { "name": "id", + "ordinal": 0, "type_info": "Uuid" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Varchar", @@ -1550,53 +1640,44 @@ "Uuid", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 AND schedule_path IS NULL AND workspace_id = $4RETURNING id" }, "77ba7207c8f5fd7156542cfd9943aa9a9fa87a652131c261f5020bab9ba6b5a3": { - "query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2", "describe": { "columns": [ { - "ordinal": 0, "name": "email", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "login_type", + "ordinal": 1, "type_info": "Text" }, { - "ordinal": 2, "name": "verified", + "ordinal": 2, "type_info": "Bool" }, { - "ordinal": 3, "name": "super_admin", + "ordinal": 3, "type_info": "Bool" }, { - "ordinal": 4, "name": "name", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "company", + "ordinal": 5, "type_info": "Varchar" } ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, "nullable": [ false, null, @@ -1604,77 +1685,86 @@ false, true, true - ] - } - }, - "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": { - "query": "SELECT * FROM resource_type WHERE workspace_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "schema", - "type_info": "Jsonb" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - } ], "parameters": { "Left": [ - "Text" + "Int8", + "Int8" ] - }, + } + }, + "query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2" + }, + "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "schema", + "ordinal": 2, + "type_info": "Jsonb" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + } + ], "nullable": [ false, false, true, true - ] - } - }, - "7eeac533a0d63f4e3af9d3e3123b0a73f44543e618e29e4c6a6d573852339933": { - "query": "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT * FROM resource_type WHERE workspace_id = $1" }, - "80325b938c9cd736f0f386343e5e36c3834521ae8e004e19c408a689dc1adcf1": { - "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6 WHERE path = $7 AND workspace_id = $8 RETURNING path", + "7eeac533a0d63f4e3af9d3e3123b0a73f44543e618e29e4c6a6d573852339933": { "describe": { "columns": [ { + "name": "name", "ordinal": 0, - "name": "path", "type_info": "Varchar" } ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc" + }, + "80325b938c9cd736f0f386343e5e36c3834521ae8e004e19c408a689dc1adcf1": { + "describe": { + "columns": [ + { + "name": "path", + "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ "Varchar", @@ -1686,16 +1776,14 @@ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6 WHERE path = $7 AND workspace_id = $8 RETURNING path" }, "808ff76fdb74482d5d3201417c8a2470e2867cb08b9a46d244653eb366d8ee5e": { - "query": "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1703,14 +1791,14 @@ "Text", "Jsonb" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)" }, "8114333781add802159cd2c26d57cea40e27143606c71bf4d87dc14bb7040cd6": { - "query": "UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -1718,747 +1806,49 @@ "Varchar", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4" }, "826c6a36020f402169cab2ebe097e9f38f416f6f080a2fe8a9b83e97c4e15150": { - "query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash", "describe": { "columns": [ { - "ordinal": 0, "name": "hash", + "ordinal": 0, "type_info": "Int8" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash" }, "82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": { - "query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3" }, - "88a3f58a1a315200fdd2e4bb8638246ee21818f8aaaf56f6e9d7ddce1490d886": { - "query": "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - } - }, - "88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": { - "query": "SELECT login_type::TEXT FROM password WHERE email = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "login_type", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - null - ] - } - }, - "894336809c161cdbb42ea235eb88498db6b1715386f78f57568f01c9954f05af": { - "query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "?column?", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [ - null - ] - } - }, - "8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": { - "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "schema", - "type_info": "Jsonb" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - true, - true - ] - } - }, - "8caa01546506f42740b7973a3a45e40093a06714b8524570c5143b77af4a8e19": { - "query": "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "schedule", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "offset_", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "args", - "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "is_flow", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false - ] - } - }, - "8d631abe38ae964edb357217463a2c1617d4b3b18a7ed0724ad1c65b95980a2c": { - "query": "SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "value", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "8ede6fb740b145b3a8320adb789500870c7a8ec807a7b156b9ff7a15791b78f8": { - "query": "DELETE FROM usr WHERE workspace_id = $1 AND username = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - } - }, - "902961f15b8c7603dddf2933b5fc7cdd6e5af3545835763ccb29cdf3ac273ef0": { - "query": "UPDATE password SET password_hash = $1 WHERE email = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Text" - ] - }, - "nullable": [] - } - }, - "904457944cbfdcefa1934059bbfea015a411c739124cf06367975cfd9cd0dd0d": { - "query": "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "value", - "type_info": "Jsonb" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "resource_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_oauth", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - true, - true, - false, - false, - false - ] - } - }, - "90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": { - "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "login_type", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "super_admin", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "verified", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "company", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - null, - false, - false, - true, - true - ] - } - }, - "92ee6274b2287bc4d368bfdb47d6452df75bd265cdd891f55112df2795294dac": { - "query": "SELECT COUNT(id) FROM queue WHERE permissioned_as = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - } - }, - "9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": { - "query": "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "value", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - true - ] - } - }, - "9a581f49d34d62550e58e6210b4bd24b7db499cc5e0350c0ce7024b3d59b13ab": { - "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name)\n VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - } - }, - "a34066d4a1578a13b2e322e6936ae80a0239a79148f3edce65f51a93910a1a4b": { - "query": "SELECT email FROM usr where username = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "email", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": { - "query": "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - } - }, - "a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": { - "query": "INSERT INTO workspace_settings\n (workspace_id)\n VALUES ($1)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar" - ] - }, - "nullable": [] - } - }, - "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb": { - "query": "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "edited_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "schedule", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "offset_", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "enabled", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "script_path", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "args", - "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "extra_perms", - "type_info": "Jsonb" - }, - { - "ordinal": 10, - "name": "is_flow", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false - ] - } - }, - "abc9f034e62ac224894173356aa69e09f3647a45d176e253e4fc8f7206f6a18d": { - "query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "summary", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "extra_perms", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - true, - false - ] - } - }, - "add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43": { - "query": "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - } - }, - "b05c5f62ef4aa21d33369130cced0e9d7d128727eb58a9be7ae69cbb16bcbb27": { - "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, now() + ($4 || ' hours')::interval, $5)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Text", - "Bool" - ] - }, - "nullable": [] - } - }, - "b20977e70ebac7ccbaec5a2a1e940301dd331a5f9a4be67a27cfbff8619ac8f0": { - "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, true)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - } - }, - "b24c1a2587812d2fd40063328ce393377826b170a7970ba02f4057454058321b": { - "query": "UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Text", - "Text" - ] - }, - "nullable": [] - } - }, - "b3b80de52d0931a2fdb5d38b7603a2d69cc25ab1cda413228c363a5ffd777113": { - "query": "SELECT * from workspace_invite WHERE workspace_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "is_admin", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false - ] - } - }, - "b7dd791cd69748ef51b7520f505c0c8bb1b4014a273476eddfecf1ab658a18b4": { - "query": "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND\n deleted = false", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "hash", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "b87f19d248b7c5dbb57e9c6c3e4dd8a5bbb70815c06ee8e5bc8d57324eea1617": { - "query": "SELECT (flow_status->'step')::integer FROM queue WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "int4", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - } - }, - "b8a0f0cc7bb87300ebffe77d8fc2b08d336b0f36659fc4a83e579461fca72d65": { - "query": "INSERT INTO completed_job as cj\n (workspace_id, id, parent_job, created_by, created_at, started_at, duration_ms, success, script_hash, script_path, args, result, logs, raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, is_skipped, language)\n VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(milliseconds FROM (now() - $6)), $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)", + "850e97afdf07f7d2bbfe33b0aaecceef6d7c736b91aaaaf60d764bd070250cba": { "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -2479,7 +1869,6 @@ "Text", { "Custom": { - "name": "job_kind", "kind": { "Enum": [ "script", @@ -2489,7 +1878,8 @@ "flowpreview", "script_hub" ] - } + }, + "name": "job_kind" } }, "Varchar", @@ -2500,246 +1890,984 @@ "Bool", { "Custom": { - "name": "script_lang", "kind": { "Enum": [ "python3", "deno" ] - } + }, + "name": "script_lang" } } ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language )\n VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(milliseconds FROM (now() - $6)), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" }, - "bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": { - "query": "DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id", + "88a3f58a1a315200fdd2e4bb8638246ee21818f8aaaf56f6e9d7ddce1490d886": { "describe": { "columns": [ { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)" + }, + "88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": { + "describe": { + "columns": [ + { + "name": "login_type", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT login_type::TEXT FROM password WHERE email = $1" + }, + "894336809c161cdbb42ea235eb88498db6b1715386f78f57568f01c9954f05af": { + "describe": { + "columns": [ + { + "name": "?column?", "ordinal": 0, - "name": "id", "type_info": "Int4" } ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + } + }, + "query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2" + }, + "8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "schema", + "ordinal": 2, + "type_info": "Jsonb" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + true, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name" + }, + "8caa01546506f42740b7973a3a45e40093a06714b8524570c5143b77af4a8e19": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + } + }, + "query": "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3" + }, + "8d631abe38ae964edb357217463a2c1617d4b3b18a7ed0724ad1c65b95980a2c": { + "describe": { + "columns": [ + { + "name": "value", + "ordinal": 0, + "type_info": "Jsonb" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" + }, + "8ede6fb740b145b3a8320adb789500870c7a8ec807a7b156b9ff7a15791b78f8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "DELETE FROM usr WHERE workspace_id = $1 AND username = $2" + }, + "902961f15b8c7603dddf2933b5fc7cdd6e5af3545835763ccb29cdf3ac273ef0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + } + }, + "query": "UPDATE password SET password_hash = $1 WHERE email = $2" + }, + "904457944cbfdcefa1934059bbfea015a411c739124cf06367975cfd9cd0dd0d": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "value", + "ordinal": 2, + "type_info": "Jsonb" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "resource_type", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "extra_perms", + "ordinal": 5, + "type_info": "Jsonb" + }, + { + "name": "is_oauth", + "ordinal": 6, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + true, + true, + false, + false, + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" + }, + "90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": { + "describe": { + "columns": [ + { + "name": "email", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "login_type", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "super_admin", + "ordinal": 2, + "type_info": "Bool" + }, + { + "name": "verified", + "ordinal": 3, + "type_info": "Bool" + }, + { + "name": "name", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "company", + "ordinal": 5, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + null, + false, + false, + true, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1" + }, + "92ee6274b2287bc4d368bfdb47d6452df75bd265cdd891f55112df2795294dac": { + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT COUNT(id) FROM queue WHERE permissioned_as = $1 AND workspace_id = $2" + }, + "9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": { + "describe": { + "columns": [ + { + "name": "value", + "ordinal": 0, + "type_info": "Jsonb" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" + }, + "9a581f49d34d62550e58e6210b4bd24b7db499cc5e0350c0ce7024b3d59b13ab": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + } + }, + "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name)\n VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3" + }, + "a34066d4a1578a13b2e322e6936ae80a0239a79148f3edce65f51a93910a1a4b": { + "describe": { + "columns": [ + { + "name": "email", + "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT email FROM usr where username = $1 AND workspace_id = $2" + }, + "a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))" + }, + "a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar" + ] + } + }, + "query": "INSERT INTO workspace_settings\n (workspace_id)\n VALUES ($1)" + }, + "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2" + }, + "abc9f034e62ac224894173356aa69e09f3647a45d176e253e4fc8f7206f6a18d": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "summary", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "extra_perms", + "ordinal": 3, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + true, + false + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + } + }, + "query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3" + }, + "add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + } + }, + "query": "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)" + }, + "b05c5f62ef4aa21d33369130cced0e9d7d128727eb58a9be7ae69cbb16bcbb27": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Text", + "Bool" + ] + } + }, + "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, now() + ($4 || ' hours')::interval, $5)" + }, + "b20977e70ebac7ccbaec5a2a1e940301dd331a5f9a4be67a27cfbff8619ac8f0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + } + }, + "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, true)" + }, + "b24c1a2587812d2fd40063328ce393377826b170a7970ba02f4057454058321b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Bool", + "Text", + "Text" + ] + } + }, + "query": "UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3" + }, + "b3b80de52d0931a2fdb5d38b7603a2d69cc25ab1cda413228c363a5ffd777113": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "is_admin", + "ordinal": 2, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT * from workspace_invite WHERE workspace_id = $1" + }, + "b7dd791cd69748ef51b7520f505c0c8bb1b4014a273476eddfecf1ab658a18b4": { + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND\n deleted = false" + }, + "b87f19d248b7c5dbb57e9c6c3e4dd8a5bbb70815c06ee8e5bc8d57324eea1617": { + "describe": { + "columns": [ + { + "name": "int4", + "ordinal": 0, + "type_info": "Int4" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "SELECT (flow_status->'step')::integer FROM queue WHERE id = $1" + }, + "bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int4" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Int4" ] - }, + } + }, + "query": "DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id" + }, + "bc01d5ca0138527b796a373f76404b8b6e4c6d711d0f6a557f929d831b8cfd3e": { + "describe": { + "columns": [ + { + "name": "?column?", + "ordinal": 0, + "type_info": "Text" + } + ], "nullable": [ - false - ] - } + null + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE" }, "bf1d8e043338867e1da1ed236ff6c85a566d5fd58d4b0d5c3a10454513811ba3": { - "query": "UPDATE workspace_settings\n SET slack_team_id = null, slack_name = null WHERE workspace_id = $1", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE workspace_settings\n SET slack_team_id = null, slack_name = null WHERE workspace_id = $1" }, "bf2aeb9a1e649106d2a084c1d628690a44573c1869a206474811215714ba97c2": { - "query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2" }, "c213427a32e8903fff649a3e7aa8392d2215665ed04f5a8f2178861e9dba298a": { - "query": "UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "path", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "edited_by", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "edited_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "schedule", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "offset_", + "ordinal": 5, "type_info": "Int4" }, { - "ordinal": 6, "name": "enabled", + "ordinal": 6, "type_info": "Bool" }, { - "ordinal": 7, "name": "script_path", + "ordinal": 7, "type_info": "Varchar" }, { - "ordinal": 8, "name": "args", + "ordinal": 8, "type_info": "Jsonb" }, { - "ordinal": 9, "name": "extra_perms", + "ordinal": 9, "type_info": "Jsonb" }, { - "ordinal": 10, "name": "is_flow", + "ordinal": 10, "type_info": "Bool" } ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], "parameters": { "Left": [ "Bool", "Text", "Text" ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false - ] - } + } + }, + "query": "UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *" }, - "c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": { - "query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username \n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2", + "c2849e67b9fea0dc46e6d7000f5a0c9dab89ae80a183d9255f8fdb356b4bc61c": { "describe": { "columns": [ { + "name": "result", "ordinal": 0, + "type_info": "Jsonb" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "SELECT result FROM completed_job WHERE id = $1" + }, + "c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": { + "describe": { + "columns": [ + { "name": "username", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username \n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2" }, "c59dd666b9a316c027e8c319b80ccbab3a220d93b64357981bd4a03324dad1d0": { - "query": "SELECT is_secret from variable WHERE path = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "is_secret", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT is_secret from variable WHERE path = $1 AND workspace_id = $2" }, "c68d39492686d0fd275acf4eca3844c37d0f58647a100d24afcf090b4b13f85d": { - "query": "SELECT username from usr WHERE workspace_id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "username", + "ordinal": 0, "type_info": "Varchar" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT username from usr WHERE workspace_id = $1" }, "c78b52b350193135d2b9fcf04d3113f7504cc61f6414f809d8a9c51a16f20d35": { - "query": "UPDATE queue SET running = false WHERE last_ping < now() - ($1 || ' seconds')::interval and running = true RETURNING id, workspace_id, last_ping", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Uuid" }, { - "ordinal": 1, "name": "workspace_id", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "last_ping", + "ordinal": 2, "type_info": "Timestamptz" } ], + "nullable": [ + false, + false, + false + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - false, - false, - false - ] - } + } + }, + "query": "UPDATE queue SET running = false WHERE last_ping < now() - ($1 || ' seconds')::interval and running = true RETURNING id, workspace_id, last_ping" }, "cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": { - "query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", @@ -2747,406 +2875,418 @@ "Text", "Int4" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4" }, "cb12aee5f8e04cb196d4b8fad81699fcbb1ae7b0c84090d5705b14eac76074ff": { - "query": "INSERT INTO group_\n VALUES ($1, 'all', 'The group that always contains all users of this workspace')", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO group_\n VALUES ($1, 'all', 'The group that always contains all users of this workspace')" }, "d2dcf69b20488d610599c309862722f805049e479035be6a416d05d73528a8e1": { - "query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Varchar", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING" }, "d4eb7aea60894b65498144b9bf522beba612f36368d62fe4e94b5b9e26349d32": { - "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], - "parameters": { - "Left": [] - }, "nullable": [ null - ] - } + ], + "parameters": { + "Left": [] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')" }, "d5d97005eebcb2760216dbd10872acdb42868fd5f7a27f71836e7a6ff1c69856": { - "query": "DELETE FROM resource_type WHERE name = $1 AND workspace_id = $2", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM resource_type WHERE name = $1 AND workspace_id = $2" }, "d697b7311430e7bd5375ec5494179f4071e3bfe123d9600249cfa80c1103edd8": { - "query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Int8", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3" }, "d768bbc46f8a9c4289b918c88ca50aa180b29bbe931d948f6e61976f71b7cdb9": { - "query": "SELECT premium FROM workspace WHERE id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "premium", + "ordinal": 0, "type_info": "Bool" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false - ] - } - }, - "d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc": { - "query": "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "workspace_id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "summary", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "extra_perms", - "type_info": "Jsonb" - } ], "parameters": { "Left": [ - "Text", "Text" ] - }, + } + }, + "query": "SELECT premium FROM workspace WHERE id = $1" + }, + "d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "summary", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "extra_perms", + "ordinal": 3, + "type_info": "Jsonb" + } + ], "nullable": [ false, false, true, false - ] - } - }, - "dcf921bf9cc7dcd9122e65b2d624a9cbbecab7275ce6b5060f4c632c53b546ae": { - "query": "SELECT content FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND (workspace_id = $2 OR workspace_id = 'starter'))", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "content", - "type_info": "Text" - } ], "parameters": { "Left": [ "Text", "Text" ] - }, + } + }, + "query": "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2" + }, + "dcf921bf9cc7dcd9122e65b2d624a9cbbecab7275ce6b5060f4c632c53b546ae": { + "describe": { + "columns": [ + { + "name": "content", + "ordinal": 0, + "type_info": "Text" + } + ], "nullable": [ false - ] - } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT content FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND (workspace_id = $2 OR workspace_id = 'starter'))" }, "dd60eb23701e97460e307b6152404e939e5bbf22d425cadd876f629134c4a683": { - "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)" }, "df8f9b2e601b0157759e9507308ea7df562a7a42516be0fcb465b8e34de0f438": { - "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE workspace.id = $1)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE workspace.id = $1)" }, "e262f83b672a558092dc959b28a919f18e44b2a0d03b27b0ddba99896940c6d3": { - "query": "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)" }, "e3eeda2e19bfbfd5aadd71c40774f7e93c8479a777fdb2828607b1db36361726": { - "query": "INSERT INTO usr_to_group\n VALUES ($1, 'all', $2)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO usr_to_group\n VALUES ($1, 'all', $2)" }, "e5231634fbb37d45e5cf3e5dfb39cb829d224caad1c3ca1712d0fc4c495aa4fc": { - "query": "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "hash", + "ordinal": 0, "type_info": "Int8" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2" }, "e587299612077e6de39b5a501aa8a736cae4a21f11853f8ae66a610b4e65f49e": { - "query": "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))", "describe": { "columns": [ { - "ordinal": 0, "name": "exists", + "ordinal": 0, "type_info": "Bool" } ], + "nullable": [ + null + ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))" }, "e69142610bd94f6c65022cca5ccf7458b7c5bb77b3ed0278715222dd920d8644": { - "query": "\n SELECT SUM(duration_ms)\n FROM completed_job\n WHERE permissioned_as = $1\n AND created_at > NOW() - INTERVAL '1200 seconds'\n AND workspace_id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "sum", + "ordinal": 0, "type_info": "Int8" } ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, "nullable": [ null - ] - } - }, - "ee99cb974fb35d2d92fa96cecb53b8ad3a729ac710f362dc55a576613b66f9be": { - "query": "SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } ], "parameters": { "Left": [ "Text", "Text" ] - }, - "nullable": [ - null - ] - } + } + }, + "query": "\n SELECT SUM(duration_ms)\n FROM completed_job\n WHERE permissioned_as = $1\n AND created_at > NOW() - INTERVAL '1200 seconds'\n AND workspace_id = $2" }, - "f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": { - "query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", + "ee99cb974fb35d2d92fa96cecb53b8ad3a729ac710f362dc55a576613b66f9be": { "describe": { "columns": [ { + "name": "exists", "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)" + }, + "f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": { + "describe": { + "columns": [ + { "name": "result", + "ordinal": 0, "type_info": "Jsonb" } ], + "nullable": [ + true + ], "parameters": { "Left": [ "Uuid", "Text" ] - }, - "nullable": [ - true - ] - } + } + }, + "query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2" }, "f325a1262084bd3468e12dc8bcc289a96536f172b679af54dd0fbc82d4d7c987": { - "query": "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Text", "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3" }, "f4a1bb4dfeafa3f28385c19d10730f2f49b4ebe5bba631cee46c760cee2e6949": { - "query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "path", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "edited_by", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "edited_at", + "ordinal": 3, "type_info": "Timestamptz" }, { - "ordinal": 4, "name": "schedule", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "offset_", + "ordinal": 5, "type_info": "Int4" }, { - "ordinal": 6, "name": "enabled", + "ordinal": 6, "type_info": "Bool" }, { - "ordinal": 7, "name": "script_path", + "ordinal": 7, "type_info": "Varchar" }, { - "ordinal": 8, "name": "args", + "ordinal": 8, "type_info": "Jsonb" }, { - "ordinal": 9, "name": "extra_perms", + "ordinal": 9, "type_info": "Jsonb" }, { - "ordinal": 10, "name": "is_flow", + "ordinal": 10, "type_info": "Bool" } ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ], "parameters": { "Left": [ "Varchar", @@ -3159,89 +3299,71 @@ "Jsonb", "Bool" ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false - ] - } + } + }, + "query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *" }, "f7906298e4204ad55ec84021bb2461f369386493519637279f1188227230c580": { - "query": "SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", "describe": { "columns": [ { - "ordinal": 0, "name": "lock", + "ordinal": 0, "type_info": "Text" }, { - "ordinal": 1, "name": "lock_error_logs", + "ordinal": 1, "type_info": "Text" } ], + "nullable": [ + true, + true + ], "parameters": { "Left": [ "Int8", "Text" ] - }, - "nullable": [ - true, - true - ] - } + } + }, + "query": "SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')" }, "f7f2598ac824e9b5719b96b1d1873ad728f7f786c7f447df6cac3e65d5a53bf1": { - "query": "SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "name", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "owner", + "ordinal": 2, "type_info": "Varchar" }, { - "ordinal": 3, "name": "domain", + "ordinal": 3, "type_info": "Varchar" }, { - "ordinal": 4, "name": "deleted", + "ordinal": 4, "type_info": "Bool" }, { - "ordinal": 5, "name": "premium", + "ordinal": 5, "type_info": "Bool" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, false, @@ -3249,68 +3371,68 @@ true, false, false - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false" }, "f98046a2ee4ac10d9e507c033391dfb0c704dcd513d8b1e5564def4e85f9e80b": { - "query": "INSERT INTO worker_ping (worker_instance, worker, ip) VALUES ($1, $2, $3)", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Varchar", "Varchar" ] - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO worker_ping (worker_instance, worker, ip) VALUES ($1, $2, $3)" }, "fa258894bd90ea6586669e5810c5c6bcb42d5e1e68fab27fb185d06962b6454a": { - "query": "SELECT * FROM resource WHERE workspace_id = $1", "describe": { "columns": [ { - "ordinal": 0, "name": "workspace_id", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "path", + "ordinal": 1, "type_info": "Varchar" }, { - "ordinal": 2, "name": "value", + "ordinal": 2, "type_info": "Jsonb" }, { - "ordinal": 3, "name": "description", + "ordinal": 3, "type_info": "Text" }, { - "ordinal": 4, "name": "resource_type", + "ordinal": 4, "type_info": "Varchar" }, { - "ordinal": 5, "name": "extra_perms", + "ordinal": 5, "type_info": "Jsonb" }, { - "ordinal": 6, "name": "is_oauth", + "ordinal": 6, "type_info": "Bool" } ], - "parameters": { - "Left": [ - "Text" - ] - }, "nullable": [ false, false, @@ -3319,19 +3441,27 @@ false, false, false - ] - } + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT * FROM resource WHERE workspace_id = $1" }, "fa567c205929f41799a64aefac97c4504c7993b22478530e9345b9bc117e92e9": { - "query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id", "describe": { "columns": [ { - "ordinal": 0, "name": "id", + "ordinal": 0, "type_info": "Int4" } ], + "nullable": [ + false + ], "parameters": { "Left": [ "Varchar", @@ -3340,51 +3470,49 @@ "Text", "Varchar" ] - }, - "nullable": [ - false - ] - } + } + }, + "query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id" }, "fbe14569717b4c4937b74e6dc6f6b4ea29a2b0c2b1cde48ef06d687f6f9e8f15": { - "query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2", "describe": { "columns": [ { - "ordinal": 0, "name": "client", + "ordinal": 0, "type_info": "Varchar" }, { - "ordinal": 1, "name": "refresh_token", + "ordinal": 1, "type_info": "Varchar" } ], + "nullable": [ + false, + false + ], "parameters": { "Left": [ "Text", "Int4" ] - }, - "nullable": [ - false, - false - ] - } + } + }, + "query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2" }, "fcb6ce4fab662602827281e3e85fffaa48542f8d590100d0e7eead2ad4ad4873": { - "query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3", "describe": { "columns": [], + "nullable": [], "parameters": { "Left": [ "Varchar", "Text", "Text" ] - }, - "nullable": [] - } + } + }, + "query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3" } } \ No newline at end of file diff --git a/backend/src/error.rs b/backend/src/error.rs index e36dc713f4..3421f74e4f 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -78,3 +78,13 @@ impl IntoResponse for Error { .unwrap() } } + +pub trait OrElseNotFound { + fn or_else_not_found(self, s: impl ToString) -> Result; +} + +impl OrElseNotFound for Option { + fn or_else_not_found(self, s: impl ToString) -> Result { + self.ok_or_else(|| Error::NotFound(s.to_string())) + } +} diff --git a/backend/src/fixtures/base.sql b/backend/src/fixtures/base.sql index 4ba98c8c64..4c3d64b535 100644 --- a/backend/src/fixtures/base.sql +++ b/backend/src/fixtures/base.sql @@ -5,11 +5,12 @@ INSERT INTO workspace (id, name, owner, domain) VALUES ('test-workspace', 'test-workspace', 'test-user', null); + CREATE FUNCTION "notify_insert_on_completed_job" () RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('insert on completed_job', NEW.id::text); - RETURN new; + RETURN NEW; END; $$ LANGUAGE PLPGSQL; @@ -17,3 +18,23 @@ $$ LANGUAGE PLPGSQL; AFTER INSERT ON "completed_job" FOR EACH ROW EXECUTE FUNCTION "notify_insert_on_completed_job" (); + + +CREATE FUNCTION "notify_queue" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('queue', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_queue_after_insert" + AFTER INSERT ON "queue" + FOR EACH ROW +EXECUTE FUNCTION "notify_queue" (); + + CREATE TRIGGER "notify_queue_after_flow_status_update" + AFTER UPDATE ON "queue" + FOR EACH ROW + WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status) +EXECUTE FUNCTION "notify_queue" (); diff --git a/backend/src/flows.rs b/backend/src/flows.rs index 7b471f9509..277429a1b5 100644 --- a/backend/src/flows.rs +++ b/backend/src/flows.rs @@ -159,6 +159,9 @@ pub struct FlowModule { pub value: FlowModuleValue, pub stop_after_if: Option, pub summary: Option, + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub suspend: u16, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] @@ -495,6 +498,7 @@ mod tests { value: FlowModuleValue::Script { path: "test".to_string() }, stop_after_if: None, summary: None, + suspend: Default::default(), }, FlowModule { input_transforms: HashMap::new(), @@ -508,6 +512,7 @@ mod tests { skip_if_stopped: false, }), summary: None, + suspend: Default::default(), }, FlowModule { input_transforms: [( @@ -525,6 +530,7 @@ mod tests { skip_if_stopped: false, }), summary: None, + suspend: Default::default(), }, ], failure_module: Some(FlowModule { @@ -535,6 +541,7 @@ mod tests { skip_if_stopped: false, }), summary: None, + suspend: Default::default(), }), retry: Default::default(), }; diff --git a/backend/src/jobs.rs b/backend/src/jobs.rs index 080c4788c6..9e9f1e6c66 100644 --- a/backend/src/jobs.rs +++ b/backend/src/jobs.rs @@ -8,6 +8,7 @@ use axum::extract::Host; +use anyhow::Context; use sql_builder::prelude::*; use sqlx::{query_scalar, Postgres, Transaction}; use std::collections::HashMap; @@ -16,23 +17,23 @@ use tracing::instrument; use crate::{ audit::{audit_log, ActionKind}, db::{UserDB, DB}, - error, - error::{to_anyhow, Error}, + error::{self, to_anyhow, Error, OrElseNotFound}, flows::FlowValue, schedule::get_schedule_opt, scripts::{get_hub_script_by_path, ScriptHash, ScriptLang}, users::{owner_to_token_owner, Authed}, - utils::{require_admin, Pagination, StripPath, now_from_db}, + utils::{now_from_db, require_admin, Pagination, StripPath}, worker, - worker_flow::{init_flow_status, FlowStatus}, + worker_flow::{init_flow_status, FlowStatus, FlowStatusModule}, }; use axum::{ - extract::{Extension, Path, Query}, + extract::{Extension, FromRequest, Path, Query}, + response::{IntoResponse, Response}, routing::{get, post}, Json, Router, }; use hyper::StatusCode; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{Map, Value}; use sql_builder::SqlBuilder; @@ -68,6 +69,14 @@ pub fn workspaced_service() -> Router { .route("/getupdate/:id", get(get_job_update)) } +pub fn global_service() -> Router { + Router::new() + .route("/resume/:id", get(resume_suspended_job)) + .route("/resume/:id", post(resume_suspended_job)) + .route("/cancel/:id", get(cancel_suspended_job)) + .route("/cancel/:id", post(cancel_suspended_job)) +} + #[derive(Debug, sqlx::FromRow, Serialize, Clone)] pub struct QueuedJob { pub workspace_id: String, @@ -103,18 +112,6 @@ impl QueuedJob { .map(String::as_str) .unwrap_or("NO_FLOW_PATH") } - - pub fn parse_raw_flow(&self) -> Option { - self.raw_flow - .as_ref() - .and_then(|v| serde_json::from_value::(v.clone()).ok()) - } - - pub fn parse_flow_status(&self) -> Option { - self.flow_status - .as_ref() - .and_then(|v| serde_json::from_value::(v.clone()).ok()) - } } #[derive(Debug, sqlx::FromRow, Serialize)] @@ -268,7 +265,7 @@ pub async fn run_wait_result_job_by_path( let script_path = script_path.to_path(); let mut tx = user_db.clone().begin(&authed).await?; let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?; - let scheduled_for = run_query.get_scheduled_for(&mut tx).await?; + let scheduled_for = run_query.get_scheduled_for(&mut tx).await?; let (uuid, tx) = push( tx, @@ -677,7 +674,7 @@ fn list_completed_jobs_query( if let Some(sk) = &lq.is_skipped { sqlb.and_where_eq("is_skipped", sk); } - if let Some(fs) = &lq.is_flow_step { + if let Some(fs) = &lq.is_flow_step { sqlb.and_where_eq("is_flow_step", fs); } if let Some(jk) = &lq.job_kinds { @@ -959,6 +956,104 @@ pub async fn get_queued_job<'c>( Ok(r) } +pub async fn resume_suspended_job( + /* unauthed */ + Extension(db): Extension, + Path((_, job)): Path<(String, Uuid)>, + QueryOrBody(value): QueryOrBody, +) -> error::Result { + NewResumeJob { job, value, is_cancel: false } + .insert(&db) + .await?; + Ok(StatusCode::CREATED) +} + +pub async fn cancel_suspended_job( + /* unauthed */ + Extension(db): Extension, + Path((_, job)): Path<(String, Uuid)>, + QueryOrBody(value): QueryOrBody, +) -> error::Result { + NewResumeJob { job, value, is_cancel: true } + .insert(&db) + .await?; + Ok(StatusCode::CREATED) +} + +struct NewResumeJob { + pub job: Uuid, + pub value: Value, + pub is_cancel: bool, +} + +impl NewResumeJob { + async fn insert(self, db: &DB) -> error::Result<()> { + insert_resume_job(db, self).await + } +} + +async fn insert_resume_job(db: &DB, new: NewResumeJob) -> error::Result<()> { + let mut tx = db.begin().await?; + + let flow = sqlx::query!( + r#" + SELECT id, flow_status, suspend + FROM queue + WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 + UNION + SELECT parent_job from completed_job WHERE id = $1 ) + FOR UPDATE + "#, + new.job, + ) + .fetch_optional(&mut tx) + .await? + .or_else_not_found(new.job)?; + + sqlx::query!( + r#" + INSERT INTO resume_job + (id, job, flow, value, is_cancel) + VALUES ($1, $2, $3, $4, $5) + "#, + Uuid::from(Ulid::new()), + new.job, + flow.id, + new.value, + new.is_cancel, + ) + .execute(&mut tx) + .await?; + + /* If the flow is currently waiting to be resumed (`FlowStatusModule::WaitingForEvents`) + * the suspend column must be set to the number of resume messages waited on. + * + * The flow's queue row is locked in this transaction because to avoid race conditions around + * the suspend column. + * That is, a job needs one event but it hasn't arrived, a worker counts zero events before + * entering WaitingForEvents. Then this message arrives but the job isn't in WaitingForEvents + * yet so the suspend counter isn't updated. Then the job enters WaitingForEvents expecting + * one event to arrive based on the count that is no longer correct. */ + if let Some(suspend) = (0 < flow.suspend).then(|| flow.suspend - 1) { + let status = + serde_json::from_value::(flow.flow_status.context("no flow status")?) + .context("deserialize flow status")?; + if matches!(status.current_step(), Some(FlowStatusModule::WaitingForEvents { job, .. }) if job == &new.job) + { + sqlx::query!( + "UPDATE queue SET suspend = $1 WHERE id = $2", + if new.is_cancel { 0 } else { suspend }, + flow.id, + ) + .execute(&mut tx) + .await?; + } + } + + tx.commit().await?; + Ok(()) +} + #[derive(Serialize)] #[serde(tag = "type")] enum Job { @@ -1369,20 +1464,41 @@ pub async fn add_completed_job( ) -> Result { let job_id = queued_job.id.clone(); sqlx::query!( - "INSERT INTO completed_job as cj - (workspace_id, id, parent_job, created_by, created_at, started_at, duration_ms, success, \ - script_hash, script_path, args, result, logs, \ - raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, \ - permissioned_as, flow_status, raw_flow, is_flow_step, is_skipped, language) - VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(milliseconds FROM (now() - $6)), $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, \ - $18, $19, $20, $21, $22, $23, $24) + "INSERT INTO completed_job AS cj + ( workspace_id + , id + , parent_job + , created_by + , created_at + , started_at + , duration_ms + , success + , script_hash + , script_path + , args + , result + , logs + , raw_code + , canceled + , canceled_by + , canceled_reason + , job_kind + , schedule_path + , permissioned_as + , flow_status + , raw_flow + , is_flow_step + , is_skipped + , language ) + VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(milliseconds FROM (now() - $6)), $7, $8, $9,\ + $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24) ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)", queued_job.workspace_id, queued_job.id, queued_job.parent_job, queued_job.created_by, queued_job.created_at, - queued_job.started_at, + queued_job.started_at, success, queued_job.script_hash.map(|x| x.0), queued_job.script_path, @@ -1452,13 +1568,27 @@ pub async fn schedule_again_if_scheduled( } pub async fn pull(db: &DB) -> Result, crate::Error> { + /* Jobs can be started if they: + * - haven't been started before, + * running = false + * - are flows with a step that needed resume, + * suspend_until is non-null + * and suspend = 0 when the resume messages are received + * or suspend_until <= now() if it has timed out */ let job: Option = sqlx::query_as::<_, QueuedJob>( "UPDATE queue - SET running = true, started_at = now(), last_ping = now() + SET running = true + , started_at = coalesce(started_at, now()) + , last_ping = now() + , suspend_until = null WHERE id IN ( SELECT id FROM queue - WHERE running = false AND scheduled_for <= now() + WHERE ( running = false + AND scheduled_for <= now()) + OR (suspend_until IS NOT NULL + AND ( suspend <= 0 + OR suspend_until <= now())) ORDER BY scheduled_for FOR UPDATE SKIP LOCKED LIMIT 1 @@ -1486,3 +1616,45 @@ pub async fn delete_job(db: &DB, w_id: &str, job_id: Uuid) -> Result<(), crate:: tracing::debug!("Job {job_id} deletion was achieved with success: {job_removed}"); Ok(()) } + +pub struct QueryOrBody(pub D); + +#[axum::async_trait] +impl FromRequest for QueryOrBody +where + D: DeserializeOwned, + B: Send + axum::body::HttpBody, + ::Data: Send, + ::Error: Into, +{ + type Rejection = Response; + + async fn from_request( + req: &mut axum::extract::RequestParts, + ) -> Result { + return if req.method() == axum::http::Method::GET { + let Query(InPayload { payload }) = Query::from_request(req) + .await + .map_err(IntoResponse::into_response)?; + decode_payload(payload) + .map(QueryOrBody) + .map_err(|err| (StatusCode::BAD_REQUEST, format!("{err:#?}"))) + .map_err(IntoResponse::into_response) + } else { + Json::from_request(req) + .await + .map(|Json(v)| QueryOrBody(v)) + .map_err(IntoResponse::into_response) + }; + + #[derive(Deserialize)] + struct InPayload { + payload: String, + } + + fn decode_payload>(t: T) -> anyhow::Result { + let vec = base64::decode_config(&t, base64::URL_SAFE).context("invalid base64")?; + serde_json::from_slice(vec.as_slice()).context("invalid json") + } + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index becf40f861..f3b0c01f1a 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -176,6 +176,7 @@ pub async fn run_server( .nest("/schedules", schedule::global_service()) .route_layer(from_extractor::()) .route_layer(from_extractor::()) + .nest("/w/:workspace_id/jobs", jobs::global_service()) .nest( "/auth", users::make_unauthed_service().layer(Extension(argon2)), diff --git a/backend/src/worker.rs b/backend/src/worker.rs index 5d4e03d864..903d0998e7 100644 --- a/backend/src/worker.rs +++ b/backend/src/worker.rs @@ -1586,9 +1586,10 @@ pub async fn restart_zombie_jobs_periodically( #[cfg(test)] mod tests { - use sqlx::{postgres::PgListener, query_scalar}; - + use futures::Stream; use serde_json::json; + use sqlx::{postgres::PgListener, query_scalar}; + use uuid::Uuid; use crate::{ db::DB, @@ -1609,6 +1610,8 @@ mod tests { /// it's important this is unique between tests as there is one prometheus registry and /// run_worker shouldn't register the same metric with the same worker name more than once. + /// + /// this must fit in varchar(50) fn next_worker_name() -> String { use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; @@ -1618,7 +1621,15 @@ mod tests { // will be "main"... The id provides uniqueness & thread_name gives context. let id = ID.fetch_add(1, SeqCst); let thread = std::thread::current(); - let thread_name = thread.name().unwrap_or("no thread name"); + let thread_name = thread + .name() + .map(|s| { + s.len() + .checked_sub(39) + .and_then(|start| s.get(start..)) + .unwrap_or(s) + }) + .unwrap_or("no thread name"); format!("{id}/{thread_name}") } @@ -1640,6 +1651,7 @@ mod tests { input_transforms: Default::default(), stop_after_if: Default::default(), summary: Default::default(), + suspend: Default::default(), }, FlowModule { value: FlowModuleValue::ForloopFlow { @@ -1660,11 +1672,13 @@ mod tests { .into(), stop_after_if: Default::default(), summary: Default::default(), + suspend: Default::default(), }], }, input_transforms: Default::default(), stop_after_if: Default::default(), summary: Default::default(), + suspend: Default::default(), }, ], ..Default::default() @@ -1712,13 +1726,13 @@ mod tests { let result = RunJob::from(job.clone()) .arg("n", json!(123)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(json!("last step saw 123"), result); let result = RunJob::from(job.clone()) .arg("n", json!(-123)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(json!(-123), result); } @@ -1826,7 +1840,7 @@ func main(derp string) (string, error) { language: ScriptLang::Go, })) .arg("derp", json!("world")) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(result, serde_json::json!("hello world")); @@ -2054,7 +2068,7 @@ def main(): let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("n", json!(0)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert!(result["from failure module"]["error"] .as_str() @@ -2063,7 +2077,7 @@ def main(): let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("n", json!(1)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert!(result["from failure module"]["error"] .as_str() @@ -2072,7 +2086,7 @@ def main(): let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("n", json!(2)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert!(result["from failure module"]["error"] .as_str() @@ -2081,11 +2095,261 @@ def main(): let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("n", json!(3)) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(json!({ "l": [0, 1, 2] }), result); } + mod suspend_resume { + use super::*; + + use futures::{Stream, StreamExt}; + + struct ApiServer { + addr: std::net::SocketAddr, + tx: tokio::sync::oneshot::Sender<()>, + task: tokio::task::JoinHandle>, + } + + impl ApiServer { + async fn start(db: DB) -> Self { + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + + let sock = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + + let addr = sock.local_addr().unwrap(); + + use crate::jobs::{cancel_suspended_job, resume_suspended_job}; + use axum::routing::{get, post}; + + let router = axum::Router::new() + .route("/w/:workspace/jobs/resume/:id", get(resume_suspended_job)) + .route("/w/:workspace/jobs/resume/:id", post(resume_suspended_job)) + .route("/w/:workspace/jobs/cancel/:id", get(cancel_suspended_job)) + .route("/w/:workspace/jobs/cancel/:id", post(cancel_suspended_job)) + .layer(axum::extract::Extension(db)); + + let serve = axum::Server::from_tcp(sock.into_std().unwrap()) + .unwrap() + .serve(router.into_make_service()) + .with_graceful_shutdown(async { drop(rx.await) }); + + let task = tokio::task::spawn(serve); + + return Self { addr, tx, task }; + } + + async fn close(self) -> hyper::Result<()> { + let Self { task, tx, .. } = self; + drop(tx); + task.await.unwrap() + } + } + + async fn wait_until_flow_suspends( + flow: Uuid, + mut queue: impl Stream + Unpin, + db: &DB, + ) { + loop { + queue.by_ref().find(&flow).await.unwrap(); + if query_scalar("SELECT suspend > 0 FROM queue WHERE id = $1") + .bind(flow) + .fetch_one(db) + .await + .unwrap() + { + break; + } + } + } + + fn flow() -> FlowValue { + serde_json::from_value(serde_json::json!({ + "modules": [{ + "input_transform": { + "n": { "type": "javascript", "expr": "flow_input.n", }, + "port": { "type": "javascript", "expr": "flow_input.port", }, + "op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", }, + }, + "value": { + "type": "rawscript", + "language": "deno", + "content": "\ + export async function main(n, port, op) {\ + const job = Deno.env.get('WM_JOB_ID'); + const r = await fetch( + `http://localhost:${port}/w/test-workspace/jobs/${op}/${job}`,\ + {\ + method: 'POST',\ + body: JSON.stringify('from job'),\ + headers: { 'content-type': 'application/json' }\ + }\ + );\ + console.log(r); + return n + 1;\ + }", + }, + "suspend": 1, + }, { + "input_transform": { + "n": { "type": "javascript", "expr": "previous_result", }, + "resume": { "type": "javascript", "expr": "resume", }, + "resumes": { "type": "javascript", "expr": "resumes", }, + }, + "value": { + "type": "rawscript", + "language": "deno", + "content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }" + }, + "suspend": 1, + }, { + "input_transform": { + "last": { "type": "javascript", "expr": "previous_result", }, + "resume": { "type": "javascript", "expr": "resume", }, + "resumes": { "type": "javascript", "expr": "resumes", }, + }, + "value": { + "type": "rawscript", + "language": "deno", + "content": "export function main(last, resume, resumes) { return { last, resume, resumes } }" + }, + }], + })) + .unwrap() + } + + #[sqlx::test(fixtures("base"))] + async fn test(db: DB) { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await; + let port = server.addr.port(); + + let flow = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) + .arg("n", json!(1)) + .arg("port", json!(port)) + .push(&db) + .await; + + let mut completed = listen_for_completed_jobs(&db).await; + let queue = listen_for_queue(&db).await; + let db_ = db.clone(); + + in_test_worker(&db, async move { + let db = db_; + + wait_until_flow_suspends(flow, queue, &db).await; + /* The first job resumes itself. */ + let _first = completed.next().await.unwrap(); + /* ... and send a request resume it. */ + let second = completed.next().await.unwrap(); + + /* ImZyb20gdGVzdCIK = base64 "from test" */ + reqwest::get(format!( + "http://localhost:{port}/w/test-workspace/jobs/resume/{second}?payload=ImZyb20gdGVzdCIK" + )) + .await + .unwrap() + .error_for_status() + .unwrap(); + + completed.find(&flow).await.unwrap(); + }) + .await; + + server.close().await.unwrap(); + + let result = completed_job_result(flow, &db).await; + + assert_eq!( + json!({ + "last": { + "resume": "from job", + "resumes": ["from job"], + "n": 3, + }, + "resume": "from test", + "resumes": ["from test"], + }), + result + ); + + // ensure resumes are cleaned up through CASCADE when the flow is finished + assert_eq!( + 0, + query_scalar::<_, i64>("SELECT count(*) FROM resume_job") + .fetch_one(&db) + .await + .unwrap() + ); + } + + #[sqlx::test(fixtures("base"))] + async fn cancel_from_job(db: DB) { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await; + let port = server.addr.port(); + + let result = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) + .arg("n", json!(1)) + .arg("op", json!("cancel")) + .arg("port", json!(port)) + .run_until_complete(&db) + .await; + + server.close().await.unwrap(); + + assert_eq!(json!("from job"), result); + } + + #[sqlx::test(fixtures("base"))] + async fn cancel_after_suspend(db: DB) { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await; + let port = server.addr.port(); + + let flow = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) + .arg("n", json!(1)) + .arg("port", json!(port)) + .push(&db) + .await; + + let mut completed = listen_for_completed_jobs(&db).await; + let queue = listen_for_queue(&db).await; + let db_ = db.clone(); + + in_test_worker(&db, async move { + let db = db_; + + wait_until_flow_suspends(flow, queue, &db).await; + /* The first job resumes itself. */ + let _first = completed.next().await.unwrap(); + /* ... and send a request resume it. */ + let second = completed.next().await.unwrap(); + + reqwest::get(format!( + "http://localhost:{port}/w/test-workspace/jobs/cancel/{second}?payload=ImZyb20gdGVzdCIK" + )) + .await + .unwrap() + .error_for_status() + .unwrap(); + + completed.find(&flow).await.unwrap(); + }) + .await; + + server.close().await.unwrap(); + + let result = completed_job_result(flow, &db).await; + + assert_eq!(json!("from test"), result); + } + } + mod retry { use super::*; @@ -2217,7 +2481,7 @@ def main(last, port): let result = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) .arg("items", json!(["unused", "unused", "unused"])) .arg("port", json!(server.addr.port())) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(server.close().await, attempts); @@ -2244,7 +2508,7 @@ def main(last, port): let result = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) .arg("items", json!(["unused", "unused", "unused"])) .arg("port", json!(server.addr.port())) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(server.close().await, attempts); @@ -2276,7 +2540,7 @@ def main(last, port): let result = RunJob::from(JobPayload::RawFlow { value: flow(), path: None }) .arg("items", json!(["unused", "unused", "unused"])) .arg("port", json!(server.addr.port())) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(server.close().await, attempts); @@ -2331,7 +2595,7 @@ def main(error, port): let server = Server::start(responses).await; let result = RunJob::from(JobPayload::RawFlow { value, path: None }) .arg("port", json!(server.addr.port())) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(server.close().await, attempts); @@ -2362,7 +2626,7 @@ def main(error, port): .unwrap(); let result = RunJob::from(JobPayload::RawFlow { value, path: None }) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!( result, @@ -2401,14 +2665,14 @@ def main(error, port): let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("items", json!([])) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert_eq!(result, serde_json::json!([])); /* Don't actually test that this does 257 jobs or that will take forever. */ let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None }) .arg("items", json!((0..257).collect::>())) - .wait_until_complete(&db) + .run_until_complete(&db) .await; assert!(matches!(result, Value::Object(_))); assert!(result["error"] @@ -2434,8 +2698,7 @@ def main(error, port): self } - /// push the job, spawn a worker, wait until the job is in completed_job - async fn wait_until_complete(self, db: &DB) -> serde_json::Value { + async fn push(self, db: &DB) -> Uuid { let RunJob { payload, args } = self; let (uuid, tx) = push( @@ -2455,43 +2718,77 @@ def main(error, port): tx.commit().await.unwrap(); - run_worker_until_complete(db, &uuid).await; + uuid + } - query_scalar("SELECT result FROM completed_job WHERE id = $1") - .bind(&uuid) - .fetch_one(db) - .await - .unwrap() + /// push the job, spawn a worker, wait until the job is in completed_job + async fn run_until_complete(self, db: &DB) -> serde_json::Value { + let uuid = self.push(db).await; + let listener = listen_for_completed_jobs(db).await; + in_test_worker(db, listener.find(&uuid)).await; + completed_job_result(uuid, db).await } } async fn run_job_in_new_worker_until_complete(db: &DB, job: JobPayload) -> serde_json::Value { - RunJob::from(job).wait_until_complete(db).await + RunJob::from(job).run_until_complete(db).await } - async fn run_worker_until_complete(db: &DB, wait_for: &uuid::Uuid) { - let mut listener = PgListener::connect_with(db).await.unwrap(); - listener.listen("insert on completed_job").await.unwrap(); + /// Start a worker with a timeout and run a future, until the worker quits or we time out. + /// + /// Cleans up the worker before resolving. + async fn in_test_worker( + db: &DB, + inner: Fut, + ) -> ::Output { + let (quit, worker) = spawn_test_worker(db); + let worker = tokio::time::timeout(std::time::Duration::from_secs(19), worker); + tokio::pin!(worker); + let res = tokio::select! { + biased; + res = inner => res, + res = &mut worker => match + res.expect("worker timed out") + .expect("worker panicked") { + _ => panic!("worker quit early"), + }, + }; + + /* ensure the worker quits before we return */ + drop(quit); + + let _: () = worker + .await + .expect("worker timed out") + .expect("worker panicked"); + + res + } + + fn spawn_test_worker( + db: &DB, + ) -> ( + tokio::sync::broadcast::Sender<()>, + tokio::task::JoinHandle<()>, + ) { let (tx, rx) = tokio::sync::broadcast::channel(1); - /* drop tx at the end of this block to close the channel and stop the worker */ - - let worker = { - let timeout = 4_000; - let worker_instance: &str = "test worker instance"; - let worker_name: String = next_worker_name(); - let i_worker: u64 = Default::default(); - let num_workers: u64 = 2; - let ip: &str = Default::default(); - let sleep_queue: u64 = DEFAULT_SLEEP_QUEUE / num_workers; - let worker_config = WorkerConfig { - base_internal_url: String::new(), - base_url: String::new(), - disable_nuser: false, - disable_nsjail: false, - keep_job_dir: false, - }; - + let db = db.to_owned(); + let timeout = 4_000; + let worker_instance: &str = "test worker instance"; + let worker_name: String = next_worker_name(); + let i_worker: u64 = Default::default(); + let num_workers: u64 = 2; + let ip: &str = Default::default(); + let sleep_queue: u64 = DEFAULT_SLEEP_QUEUE / num_workers; + let worker_config = WorkerConfig { + base_internal_url: String::new(), + base_url: String::new(), + disable_nuser: false, + disable_nsjail: false, + keep_job_dir: false, + }; + let future = async move { run_worker( &db, timeout, @@ -2504,30 +2801,62 @@ def main(error, port): worker_config, rx, ) + .await }; - let worker = tokio::time::timeout(std::time::Duration::from_secs(19), worker); - - tokio::pin!(worker); - - while wait_for - != &tokio::select! { - biased; - notify = listener.recv() => notify, - res = &mut worker => if res.is_ok() { - panic!("worker quit early") - } else { - panic!("worker timed out") - }, - } - .unwrap() - .payload() - .parse::() - .unwrap() - {} - - /* ensure the worker quits before we return */ - drop(tx); - worker.await.expect("worker timed out") + (tx, tokio::task::spawn(future)) } + + async fn listen_for_completed_jobs(db: &DB) -> impl Stream + Unpin { + listen_for_uuid_on(db, "insert on completed_job").await + } + + async fn listen_for_queue(db: &DB) -> impl Stream + Unpin { + listen_for_uuid_on(db, "queue").await + } + + async fn listen_for_uuid_on( + db: &DB, + channel: &'static str, + ) -> impl Stream + Unpin { + let mut listener = PgListener::connect_with(db).await.unwrap(); + listener.listen(channel).await.unwrap(); + + Box::pin(futures::stream::unfold( + listener, + |mut listener| async move { + let uuid = listener + .try_recv() + .await + .unwrap() + .expect("lost database connection") + .payload() + .parse::() + .expect("invalid uuid"); + Some((uuid, listener)) + }, + )) + } + + async fn completed_job_result(uuid: Uuid, db: &DB) -> Value { + query_scalar("SELECT result FROM completed_job WHERE id = $1") + .bind(uuid) + .fetch_one(db) + .await + .unwrap() + } + + #[axum::async_trait(?Send)] + trait StreamFind: futures::Stream + Unpin + Sized { + async fn find(self, item: &Self::Item) -> Option + where + for<'l> &'l Self::Item: std::cmp::PartialEq, + { + use futures::{future::ready, StreamExt}; + + self.filter(|i| ready(i == item)).next().await + } + } + + impl StreamFind for T {} } diff --git a/backend/src/worker_flow.rs b/backend/src/worker_flow.rs index 4beea57f3e..aa737cc126 100644 --- a/backend/src/worker_flow.rs +++ b/backend/src/worker_flow.rs @@ -22,8 +22,11 @@ use serde_json::{json, Map, Value}; use tracing::instrument; use uuid::Uuid; +const MINUTES: Duration = Duration::from_secs(60); +const HOURS: Duration = MINUTES.saturating_mul(60); + const MAX_RETRY_ATTEMPTS: u16 = 1000; -const MAX_RETRY_INTERVAL: Duration = /* six hours */ Duration::from_secs(6 * 60 * 60); +const MAX_RETRY_INTERVAL: Duration = HOURS.saturating_mul(6); #[derive(Serialize, Deserialize, Debug)] pub struct FlowStatus { @@ -53,7 +56,7 @@ pub struct Iterator { #[serde(tag = "type")] pub enum FlowStatusModule { WaitingForPriorSteps, - WaitingForEvent { event: String }, + WaitingForEvents { count: u16, job: Uuid }, WaitingForExecutor { job: Uuid }, InProgress { job: Uuid, iterator: Option, forloop_jobs: Option> }, Success { job: Uuid, forloop_jobs: Option> }, @@ -69,6 +72,12 @@ impl FlowStatus { retry: RetryStatus { fail_count: 0, previous_result: None }, } } + + /// current module status ... excluding failure_module + pub fn current_step(&self) -> Option<&FlowStatusModule> { + let i = usize::try_from(self.step).ok()?; + self.modules.get(i) + } } #[async_recursion] @@ -234,14 +243,8 @@ pub async fn update_flow_status_after_job_completion( false if skip_loop_failures => !is_last_step, false if next_retry( - &flow_job - .parse_raw_flow() - .map(|module| module.retry) - .unwrap_or_default(), - &flow_job - .parse_flow_status() - .map(|status| status.retry) - .unwrap_or_default(), + &flow_job.parse_raw_flow_retry().unwrap_or_default(), + &flow_job.parse_flow_status_retry().unwrap_or_default(), ) .is_some() => { @@ -404,6 +407,7 @@ pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result { Ok(r) } +/// resumes should be in order of timestamp ascending, so that more recent are at the end #[instrument(level = "trace", skip_all)] async fn transform_input( flow_args: &Option, @@ -412,6 +416,7 @@ async fn transform_input( workspace: &str, token: &str, steps: Vec, + resumes: &[Value], ) -> anyhow::Result> { let mut mapped = serde_json::Map::new(); @@ -433,6 +438,11 @@ async fn transform_input( ("params".to_string(), serde_json::json!(mapped)), ("previous_result".to_string(), previous_result), ("flow_input".to_string(), flow_input), + ( + "resume".to_string(), + resumes.last().map(|v| json!(v)).unwrap_or_default(), + ), + ("resumes".to_string(), resumes.clone().into()), ], Some(EvalCreds { workspace: workspace.to_string(), token: token.to_string() }), steps.clone(), @@ -533,6 +543,112 @@ async fn push_next_flow_job( let mut scheduled_for_o = None; + let mut resume_messages: Vec = vec![]; + + /* (suspend / resume), when starting a module, if previous module has a + * non-zero `suspend` value, collect `resume_job`s for the previous module job. + * + * If there aren't enough, try again later. */ + if matches!( + &status_module, + FlowStatusModule::WaitingForPriorSteps | FlowStatusModule::WaitingForEvents { .. } + ) { + if let Some((count, last)) = needs_resume(&flow, &status) { + let mut tx = db.begin().await?; + + /* Lock this row to prevent the suspend column getting out out of sync + * if a resume message arrives after we fetch and count them here. + * + * This only works because jobs::resume_job does the same thing. */ + sqlx::query_scalar!( + "SELECT null FROM queue WHERE id = $1 FOR UPDATE", + flow_job.id + ) + .fetch_one(&mut tx) + .await + .context("lock flow in queue")?; + + let resumes = sqlx::query!( + "SELECT value, is_cancel FROM resume_job WHERE job = $1 ORDER BY created_at ASC", + last + ) + .fetch_all(&mut tx) + .await?; + + let is_cancelled = resumes + .iter() + .find(|r| r.is_cancel) + .map(|r| r.value.clone()); + + resume_messages.extend(resumes.into_iter().map(|r| r.value)); + + if is_cancelled.is_none() && resume_messages.len() >= count as usize { + /* If we are woken up after suspending, last_result will be the flow args, but we + * should use the result from the last job */ + if let FlowStatusModule::WaitingForEvents { .. } = &status_module { + last_result = + sqlx::query_scalar!("SELECT result FROM completed_job WHERE id = $1", last) + .fetch_one(&mut tx) + .await? + .context("previous job result")?; + } + + /* continue on and run this job! */ + tx.commit().await?; + + /* not enough messages to do this job, "park"/suspend until there are */ + } else if is_cancelled.is_none() + && matches!(&status_module, FlowStatusModule::WaitingForPriorSteps) + { + sqlx::query( + " + UPDATE queue + SET flow_status = JSONB_SET(flow_status, ARRAY['modules', flow_status->>'step'::text], $1) + , suspend = $2 + , suspend_until = now() + $3 + WHERE id = $4 + ", + ) + .bind(json!(FlowStatusModule::WaitingForEvents { count, job: last })) + .bind(count as i32) + .bind(30 * MINUTES) + .bind(flow_job.id) + .execute(&mut tx) + .await?; + + tx.commit().await?; + return Ok(()); + + /* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */ + } else { + tx.commit().await?; + + let success = false; + let skipped = false; + let logs = if is_cancelled.is_some() { + "Cancelled while waiting to be resumed" + } else { + "Timed out waiting to be resumed" + } + .to_string(); + let result = is_cancelled.unwrap_or(json!({ "error": logs })); + let _uuid = + add_completed_job(db, &flow_job, success, skipped, result, logs).await?; + postprocess_queued_job( + false, + flow_job.schedule_path.clone(), + flow_job.script_path.clone(), + &flow_job.workspace_id, + flow_job.id, + db, + ) + .await?; + + return Ok(()); + } + } + } + if matches!(&status_module, FlowStatusModule::Failure { .. }) { if let Some((fail_count, retry_in)) = next_retry(&flow.retry, &status.retry) { tracing::debug!( @@ -642,6 +758,7 @@ async fn push_next_flow_job( &flow_job.workspace_id, &token, steps.to_vec(), + resume_messages.as_slice(), ) .await? } else { @@ -910,6 +1027,28 @@ impl InputTransform { } } +impl QueuedJob { + pub fn parse_raw_flow(&self) -> Option { + self.raw_flow + .as_ref() + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + } + + pub fn parse_raw_flow_retry(&self) -> Option { + self.parse_raw_flow().map(|module| module.retry) + } + + pub fn parse_flow_status(&self) -> Option { + self.flow_status + .as_ref() + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + } + + pub fn parse_flow_status_retry(&self) -> Option { + self.parse_flow_status().map(|status| status.retry) + } +} + trait IntoArray: Sized { fn into_array(self) -> Result, Self>; } @@ -931,3 +1070,21 @@ fn from_now(duration: Duration) -> chrono::DateTime { .and_then(|d| chrono::Utc::now().checked_add_signed(d)) .unwrap_or(chrono::DateTime::::MAX_UTC) } + +/// returns previous module non-zero suspend count and job +fn needs_resume(flow: &FlowValue, status: &FlowStatus) -> Option<(u16, Uuid)> { + let prev = usize::try_from(status.step) + .ok() + .and_then(|s| s.checked_sub(1))?; + + let suspend = flow.modules.get(prev)?.suspend; + if suspend == 0 { + return None; + } + + if let &FlowStatusModule::Success { job, .. } = status.modules.get(prev)? { + Some((suspend, job)) + } else { + None + } +} diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index f7ac183aca..0ae1112782 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -84,6 +84,8 @@ components: - expr summary: type: string + suspend: + type: integer required: - input_transforms - value @@ -239,8 +241,8 @@ components: job: type: string format: uuid - event: - type: string + count: + type: integer iterator: type: object properties: