@amalgm/automations 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AXIOMS.md +7 -1
- package/PURPOSE.md +3 -1
- package/README.md +4 -1
- package/dist/src/cli/shell-runtime.js +34 -5
- package/dist/src/execution-limits.d.ts +6 -0
- package/dist/src/execution-limits.js +6 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mcp.js +8 -1
- package/dist/src/run-journal.js +56 -1
- package/package.json +1 -1
- package/supabase/migrations/20260904010000_typed_workflow_admission.sql +272 -0
package/AXIOMS.md
CHANGED
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
state; it never owns a second automation-definition API.
|
|
42
42
|
15. Admitting a run atomically verifies the current enabled configuration,
|
|
43
43
|
stores a secret-free snapshot, and, for a schedule, advances exactly the
|
|
44
|
-
firing instant that was claimed.
|
|
44
|
+
firing instant that was claimed. Manual, schedule, and webhook admission
|
|
45
|
+
derive executability from the same declared set of supported workflow
|
|
46
|
+
versions.
|
|
45
47
|
16. Legacy local automation storage is not a compatibility authority. Engine
|
|
46
48
|
is deprecated read-only evidence; active callers use this SDK and its
|
|
47
49
|
standalone service, and no old store may run beside them.
|
|
@@ -123,3 +125,7 @@
|
|
|
123
125
|
credential, tunnel credential, or a standalone Automations authorization
|
|
124
126
|
value. Native tool authentication comes from that tool's own user-level
|
|
125
127
|
setup, not from Amalgm's private transport authority.
|
|
128
|
+
43. The durable step journal has one SDK-owned serialized size bound. Oversized
|
|
129
|
+
action or process results become an explicit byte count plus bounded JSON
|
|
130
|
+
preview before transport, so output can never strand a run outside the
|
|
131
|
+
ledger it is meant to update.
|
package/PURPOSE.md
CHANGED
|
@@ -58,7 +58,9 @@ the selected machine host supplies every effectful capability.
|
|
|
58
58
|
|
|
59
59
|
The machine records a step as running before invoking it and records its output
|
|
60
60
|
before advancing. Reconnect resumes at the first step that is not already
|
|
61
|
-
complete.
|
|
61
|
+
complete. One serialized journal bound applies to action and process output;
|
|
62
|
+
oversized results retain their byte count and a bounded preview, so evidence
|
|
63
|
+
cannot make its own durable update impossible. A transient network failure releases the same run back to the queue
|
|
62
64
|
with a bounded future retry time; a configuration, authorization, or execution
|
|
63
65
|
error fails it. Every execution capability receives a stable per-step
|
|
64
66
|
idempotency key. Amalgm actions can use it to make an uncertain acknowledgement
|
package/README.md
CHANGED
|
@@ -169,7 +169,10 @@ newline. Either action `input` or process `stdin` may contain the exact template
|
|
|
169
169
|
Shell injects action and process capabilities into the shared executor. Process
|
|
170
170
|
results record exit code, signal, stdout, stderr, and truncation in run history;
|
|
171
171
|
timeouts, cancellation, and output limits are enforced on the selected
|
|
172
|
-
machine.
|
|
172
|
+
machine. The durable journal begins compacting after 1 MiB and stores oversized
|
|
173
|
+
results as an explicit original byte count plus a bounded JSON preview, keeping
|
|
174
|
+
every run update within the service transport bound. Children inherit the
|
|
175
|
+
user's ordinary machine environment so installed
|
|
173
176
|
CLI auth continues to work, but never inherit Shell's private runtime, tunnel,
|
|
174
177
|
or machine credentials. Values in workflow `env` are persisted non-secret
|
|
175
178
|
configuration.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { automationCommandDefinition, } from '../command-surface.js';
|
|
2
|
+
import { MAX_CLI_RUNTIME_RESPONSE_BYTES } from '../execution-limits.js';
|
|
2
3
|
import { AutomationError, ValidationError } from '../errors.js';
|
|
3
4
|
export function createShellAutomationCommands(options) {
|
|
4
5
|
const endpoint = shellAutomationEndpoint(options.runtimeUrl);
|
|
@@ -68,21 +69,49 @@ export function shellAutomationEndpoint(runtimeUrl) {
|
|
|
68
69
|
async function jsonResponse(response) {
|
|
69
70
|
let bytes;
|
|
70
71
|
try {
|
|
71
|
-
bytes =
|
|
72
|
+
bytes = await boundedResponse(response, MAX_CLI_RUNTIME_RESPONSE_BYTES);
|
|
72
73
|
}
|
|
73
74
|
catch (error) {
|
|
75
|
+
if (error instanceof AutomationError)
|
|
76
|
+
throw error;
|
|
74
77
|
throw new AutomationError('runtime_transport', `Cannot read Amalgm Shell runtime response: ${message(error)}`);
|
|
75
78
|
}
|
|
76
|
-
if (bytes.byteLength > 2 * 1024 * 1024) {
|
|
77
|
-
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
78
|
-
}
|
|
79
79
|
try {
|
|
80
|
-
return JSON.parse(
|
|
80
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
81
81
|
}
|
|
82
82
|
catch {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
async function boundedResponse(response, maximumBytes) {
|
|
87
|
+
const declared = Number(response.headers.get('content-length'));
|
|
88
|
+
if (Number.isFinite(declared) && declared > maximumBytes) {
|
|
89
|
+
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
90
|
+
}
|
|
91
|
+
if (!response.body)
|
|
92
|
+
return Buffer.alloc(0);
|
|
93
|
+
const chunks = [];
|
|
94
|
+
let size = 0;
|
|
95
|
+
const reader = response.body.getReader();
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
const chunk = Buffer.from(value);
|
|
102
|
+
size += chunk.byteLength;
|
|
103
|
+
if (size > maximumBytes) {
|
|
104
|
+
await reader.cancel();
|
|
105
|
+
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
106
|
+
}
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
reader.releaseLock();
|
|
112
|
+
}
|
|
113
|
+
return Buffer.concat(chunks, size);
|
|
114
|
+
}
|
|
86
115
|
function toolResult(value) {
|
|
87
116
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
88
117
|
throw new AutomationError('runtime_protocol', 'Automations MCP returned an invalid tool result');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Start compacting step output before metadata can approach the service body bound. */
|
|
2
|
+
export declare const RUN_JOURNAL_OUTPUT_BUDGET_BYTES: number;
|
|
3
|
+
/** One persisted journal, including step metadata, must remain below 1.5 MiB. */
|
|
4
|
+
export declare const MAX_RUN_JOURNAL_BYTES: number;
|
|
5
|
+
/** A get response can contain the default page of twenty maximum-sized journals. */
|
|
6
|
+
export declare const MAX_CLI_RUNTIME_RESPONSE_BYTES: number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Start compacting step output before metadata can approach the service body bound. */
|
|
2
|
+
export const RUN_JOURNAL_OUTPUT_BUDGET_BYTES = 1024 * 1024;
|
|
3
|
+
/** One persisted journal, including step metadata, must remain below 1.5 MiB. */
|
|
4
|
+
export const MAX_RUN_JOURNAL_BYTES = 1536 * 1024;
|
|
5
|
+
/** A get response can contain the default page of twenty maximum-sized journals. */
|
|
6
|
+
export const MAX_CLI_RUNTIME_RESPONSE_BYTES = 32 * 1024 * 1024;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { createAutomationApi, type AuthenticateAutomationRequest, type Automatio
|
|
|
5
5
|
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, type AutomationCliIo, type AutomationCliMainOptions, type ShellAutomationCommandOptions, } from './cli.js';
|
|
6
6
|
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, type AutomationCommandAnnotations, type AutomationCommandDefinition, type AutomationCommandName, type AutomationCommands, } from './command-surface.js';
|
|
7
7
|
export { automationFailure, type AutomationFailureEnvelope } from './command-result.js';
|
|
8
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
8
9
|
export { createAutomationMcpServer } from './mcp.js';
|
|
9
10
|
export { createAutomationToolSurface, type AutomationToolSurface, type AutomationView, type CreateAutomationDefinition, type UpdateAutomationDefinition, type WorkflowChange, } from './tool-surface.js';
|
|
10
11
|
export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
|
package/dist/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { createAutomationApi } from './http.js';
|
|
|
12
12
|
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, } from './cli.js';
|
|
13
13
|
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, } from './command-surface.js';
|
|
14
14
|
export { automationFailure } from './command-result.js';
|
|
15
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
15
16
|
export { createAutomationMcpServer } from './mcp.js';
|
|
16
17
|
export { createAutomationToolSurface, } from './tool-surface.js';
|
|
17
18
|
export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
|
package/dist/src/mcp.js
CHANGED
|
@@ -4,7 +4,14 @@ import { automationCommandDefinitions, createAutomationCommands, } from './comma
|
|
|
4
4
|
import { automationFailure } from './command-result.js';
|
|
5
5
|
import { createAutomationToolSurface } from './tool-surface.js';
|
|
6
6
|
export { createAutomationToolSurface };
|
|
7
|
-
const resultSchema = {
|
|
7
|
+
const resultSchema = {
|
|
8
|
+
result: z.unknown().optional(),
|
|
9
|
+
error: z.object({
|
|
10
|
+
code: z.string(),
|
|
11
|
+
message: z.string(),
|
|
12
|
+
status: z.number().optional(),
|
|
13
|
+
}).strict().optional(),
|
|
14
|
+
};
|
|
8
15
|
export function createAutomationMcpServer(sdk) {
|
|
9
16
|
const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
|
|
10
17
|
const commands = createAutomationCommands(sdk);
|
package/dist/src/run-journal.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
2
|
+
const MAX_OUTPUT_PREVIEW_CHARACTERS = 64 * 1024;
|
|
1
3
|
export function emptyJournal() {
|
|
2
4
|
return { version: 1, steps: [] };
|
|
3
5
|
}
|
|
4
6
|
export function runJournal(value, plan) {
|
|
5
7
|
if (value === undefined)
|
|
6
8
|
return emptyJournal();
|
|
9
|
+
if (jsonBytes(value) > MAX_RUN_JOURNAL_BYTES)
|
|
10
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
7
11
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
12
|
throw new Error('Run step journal is invalid');
|
|
9
13
|
const source = value;
|
|
@@ -34,12 +38,63 @@ export function putStep(journal, step) {
|
|
|
34
38
|
steps.push(step);
|
|
35
39
|
else
|
|
36
40
|
steps[index] = step;
|
|
37
|
-
|
|
41
|
+
let candidate = { version: 1, steps };
|
|
42
|
+
if (jsonBytes(candidate) > RUN_JOURNAL_OUTPUT_BUDGET_BYTES && step.output !== undefined) {
|
|
43
|
+
const compacted = { ...step, output: compactOutput(step.output) };
|
|
44
|
+
if (index === -1)
|
|
45
|
+
steps[steps.length - 1] = compacted;
|
|
46
|
+
else
|
|
47
|
+
steps[index] = compacted;
|
|
48
|
+
candidate = { version: 1, steps };
|
|
49
|
+
}
|
|
50
|
+
if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
|
|
51
|
+
candidate = removeStoredPreviews(candidate);
|
|
52
|
+
}
|
|
53
|
+
if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
|
|
54
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
55
|
+
}
|
|
56
|
+
return candidate;
|
|
38
57
|
}
|
|
39
58
|
/** Journals are constructed from JSON fields only; this is the persistence boundary. */
|
|
40
59
|
export function journalJson(journal) {
|
|
60
|
+
if (jsonBytes(journal) > MAX_RUN_JOURNAL_BYTES)
|
|
61
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
41
62
|
return journal;
|
|
42
63
|
}
|
|
64
|
+
function compactOutput(output) {
|
|
65
|
+
const serialized = json(output);
|
|
66
|
+
return {
|
|
67
|
+
$amalgmTruncated: true,
|
|
68
|
+
originalBytes: Buffer.byteLength(serialized),
|
|
69
|
+
jsonPreview: serialized.slice(0, MAX_OUTPUT_PREVIEW_CHARACTERS),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function removeStoredPreviews(journal) {
|
|
73
|
+
return {
|
|
74
|
+
version: 1,
|
|
75
|
+
steps: journal.steps.map((step) => {
|
|
76
|
+
if (!isCompactedOutput(step.output))
|
|
77
|
+
return step;
|
|
78
|
+
const { jsonPreview: _preview, ...output } = step.output;
|
|
79
|
+
return { ...step, output };
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function isCompactedOutput(value) {
|
|
84
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
85
|
+
return false;
|
|
86
|
+
const item = value;
|
|
87
|
+
return item.$amalgmTruncated === true && typeof item.jsonPreview === 'string';
|
|
88
|
+
}
|
|
89
|
+
function jsonBytes(value) {
|
|
90
|
+
return Buffer.byteLength(json(value));
|
|
91
|
+
}
|
|
92
|
+
function json(value) {
|
|
93
|
+
const serialized = JSON.stringify(value);
|
|
94
|
+
if (typeof serialized !== 'string')
|
|
95
|
+
throw new Error('Run step output must be JSON');
|
|
96
|
+
return serialized;
|
|
97
|
+
}
|
|
43
98
|
function runStep(value) {
|
|
44
99
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
100
|
throw new Error('Run step is invalid');
|
package/package.json
CHANGED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
-- Every delivery path admits the same declared workflow versions. The SDK owns
|
|
2
|
+
-- shape validation; this transaction-bound predicate prevents incomplete or
|
|
3
|
+
-- unsupported definitions from entering the durable execution ledger.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION public.amalgm_workflow_is_executable(p_compiled jsonb)
|
|
6
|
+
RETURNS boolean
|
|
7
|
+
LANGUAGE sql
|
|
8
|
+
IMMUTABLE
|
|
9
|
+
SET search_path = public
|
|
10
|
+
AS $$
|
|
11
|
+
SELECT jsonb_typeof(p_compiled) = 'object'
|
|
12
|
+
AND p_compiled->>'version' IN ('1', '2')
|
|
13
|
+
AND jsonb_typeof(p_compiled->'steps') = 'array'
|
|
14
|
+
AND jsonb_array_length(
|
|
15
|
+
CASE WHEN jsonb_typeof(p_compiled->'steps') = 'array'
|
|
16
|
+
THEN p_compiled->'steps' ELSE '[]'::jsonb END
|
|
17
|
+
) BETWEEN 1 AND 100;
|
|
18
|
+
$$;
|
|
19
|
+
|
|
20
|
+
CREATE OR REPLACE FUNCTION public.list_amalgm_event_triggers(p_user_id uuid, p_target_id text)
|
|
21
|
+
RETURNS TABLE (
|
|
22
|
+
user_id uuid,
|
|
23
|
+
automation_id text,
|
|
24
|
+
trigger_id text,
|
|
25
|
+
target_id text,
|
|
26
|
+
source text,
|
|
27
|
+
event text,
|
|
28
|
+
secret text
|
|
29
|
+
)
|
|
30
|
+
LANGUAGE sql
|
|
31
|
+
SECURITY DEFINER
|
|
32
|
+
SET search_path = public
|
|
33
|
+
AS $$
|
|
34
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id, t.source, t.event, t.secret
|
|
35
|
+
FROM public.amalgm_automation_triggers t
|
|
36
|
+
JOIN public.amalgm_automations a
|
|
37
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
38
|
+
JOIN public.amalgm_automation_workflows w
|
|
39
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
40
|
+
WHERE t.kind = 'webhook' AND t.user_id = p_user_id
|
|
41
|
+
AND a.target_id = p_target_id AND t.enabled AND a.enabled
|
|
42
|
+
AND public.amalgm_workflow_is_executable(w.compiled);
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.list_due_amalgm_crons(p_now timestamptz)
|
|
46
|
+
RETURNS TABLE (
|
|
47
|
+
user_id uuid,
|
|
48
|
+
automation_id text,
|
|
49
|
+
trigger_id text,
|
|
50
|
+
target_id text,
|
|
51
|
+
cron text,
|
|
52
|
+
timezone text,
|
|
53
|
+
next_run_at timestamptz,
|
|
54
|
+
remaining_occurrences integer
|
|
55
|
+
)
|
|
56
|
+
LANGUAGE sql
|
|
57
|
+
SECURITY DEFINER
|
|
58
|
+
SET search_path = public
|
|
59
|
+
AS $$
|
|
60
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id,
|
|
61
|
+
t.cron, t.timezone, t.next_run_at, t.remaining_occurrences
|
|
62
|
+
FROM public.amalgm_automation_triggers t
|
|
63
|
+
JOIN public.amalgm_automations a
|
|
64
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
65
|
+
JOIN public.amalgm_automation_workflows w
|
|
66
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
67
|
+
WHERE t.kind = 'schedule' AND t.next_run_at <= p_now
|
|
68
|
+
AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
|
|
69
|
+
AND t.enabled AND a.enabled
|
|
70
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
71
|
+
ORDER BY t.next_run_at, t.user_id, t.automation_id, t.id;
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION public.resolve_amalgm_webhook(p_endpoint_id text)
|
|
75
|
+
RETURNS TABLE (
|
|
76
|
+
user_id uuid,
|
|
77
|
+
automation_id text,
|
|
78
|
+
trigger_id text,
|
|
79
|
+
target_id text,
|
|
80
|
+
endpoint_id text,
|
|
81
|
+
source text,
|
|
82
|
+
event text,
|
|
83
|
+
secret text
|
|
84
|
+
)
|
|
85
|
+
LANGUAGE sql
|
|
86
|
+
SECURITY DEFINER
|
|
87
|
+
SET search_path = public
|
|
88
|
+
AS $$
|
|
89
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id,
|
|
90
|
+
t.endpoint_id, t.source, t.event, t.secret
|
|
91
|
+
FROM public.amalgm_automation_triggers t
|
|
92
|
+
JOIN public.amalgm_automations a
|
|
93
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
94
|
+
JOIN public.amalgm_automation_workflows w
|
|
95
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
96
|
+
WHERE t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
|
|
97
|
+
AND t.enabled AND a.enabled
|
|
98
|
+
AND public.amalgm_workflow_is_executable(w.compiled);
|
|
99
|
+
$$;
|
|
100
|
+
|
|
101
|
+
CREATE OR REPLACE FUNCTION public.enqueue_amalgm_run(
|
|
102
|
+
p_kind text,
|
|
103
|
+
p_user_id uuid,
|
|
104
|
+
p_automation_id text,
|
|
105
|
+
p_trigger_id text,
|
|
106
|
+
p_target_id text,
|
|
107
|
+
p_endpoint_id text,
|
|
108
|
+
p_delivery_key text,
|
|
109
|
+
p_expected_next_run_at timestamptz,
|
|
110
|
+
p_next_run_at timestamptz,
|
|
111
|
+
p_input jsonb,
|
|
112
|
+
p_now timestamptz
|
|
113
|
+
) RETURNS SETOF public.amalgm_automation_runs
|
|
114
|
+
LANGUAGE plpgsql
|
|
115
|
+
SECURITY DEFINER
|
|
116
|
+
SET search_path = public
|
|
117
|
+
AS $$
|
|
118
|
+
DECLARE
|
|
119
|
+
v_inserted integer := 0;
|
|
120
|
+
BEGIN
|
|
121
|
+
IF p_kind = 'cron' THEN
|
|
122
|
+
IF p_endpoint_id IS NOT NULL OR p_delivery_key IS NOT NULL THEN
|
|
123
|
+
RAISE EXCEPTION 'Schedule admission cannot carry webhook identity';
|
|
124
|
+
END IF;
|
|
125
|
+
PERFORM 1
|
|
126
|
+
FROM public.amalgm_automation_triggers t
|
|
127
|
+
JOIN public.amalgm_automations a
|
|
128
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
129
|
+
WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
|
|
130
|
+
AND t.id = p_trigger_id AND a.target_id = p_target_id
|
|
131
|
+
AND t.kind = 'schedule' AND t.next_run_at = p_expected_next_run_at
|
|
132
|
+
AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
|
|
133
|
+
AND t.enabled AND a.enabled
|
|
134
|
+
FOR UPDATE OF t;
|
|
135
|
+
ELSIF p_kind = 'event' THEN
|
|
136
|
+
IF p_endpoint_id IS NULL THEN RAISE EXCEPTION 'Webhook endpoint is required'; END IF;
|
|
137
|
+
PERFORM 1
|
|
138
|
+
FROM public.amalgm_automation_triggers t
|
|
139
|
+
JOIN public.amalgm_automations a
|
|
140
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
141
|
+
WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
|
|
142
|
+
AND t.id = p_trigger_id AND a.target_id = p_target_id
|
|
143
|
+
AND t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
|
|
144
|
+
AND t.enabled AND a.enabled;
|
|
145
|
+
ELSE
|
|
146
|
+
RAISE EXCEPTION 'Invalid automation trigger kind';
|
|
147
|
+
END IF;
|
|
148
|
+
IF NOT FOUND THEN RETURN; END IF;
|
|
149
|
+
|
|
150
|
+
RETURN QUERY
|
|
151
|
+
INSERT INTO public.amalgm_automation_runs (
|
|
152
|
+
user_id, target_id, automation_id, trigger_id, workflow_id,
|
|
153
|
+
automation_payload, input, status, delivery_key, created_at
|
|
154
|
+
)
|
|
155
|
+
SELECT a.user_id, a.target_id, a.id, t.id, w.id,
|
|
156
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
157
|
+
'id', a.id, 'targetId', a.target_id, 'name', a.name,
|
|
158
|
+
'description', a.description, 'enabled', a.enabled,
|
|
159
|
+
'trigger', jsonb_strip_nulls(jsonb_build_object(
|
|
160
|
+
'id', t.id,
|
|
161
|
+
'kind', CASE WHEN t.kind = 'schedule' THEN 'cron' ELSE 'event' END,
|
|
162
|
+
'enabled', t.enabled, 'cron', t.cron, 'timezone', t.timezone,
|
|
163
|
+
'source', t.source, 'event', t.event
|
|
164
|
+
)),
|
|
165
|
+
'workflow', jsonb_strip_nulls(jsonb_build_object(
|
|
166
|
+
'id', w.id, 'name', w.name, 'script', w.script,
|
|
167
|
+
'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
|
|
168
|
+
))
|
|
169
|
+
)),
|
|
170
|
+
p_input, 'pending', p_delivery_key, p_now
|
|
171
|
+
FROM public.amalgm_automations a
|
|
172
|
+
JOIN public.amalgm_automation_triggers t
|
|
173
|
+
ON t.user_id = a.user_id AND t.automation_id = a.id
|
|
174
|
+
JOIN public.amalgm_automation_workflows w
|
|
175
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
176
|
+
WHERE a.user_id = p_user_id AND a.id = p_automation_id AND t.id = p_trigger_id
|
|
177
|
+
AND a.target_id = p_target_id AND t.enabled AND a.enabled
|
|
178
|
+
AND (p_kind = 'cron' OR (t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id))
|
|
179
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
180
|
+
ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
|
|
181
|
+
WHERE delivery_key IS NOT NULL
|
|
182
|
+
DO UPDATE SET delivery_key = EXCLUDED.delivery_key
|
|
183
|
+
RETURNING *;
|
|
184
|
+
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
185
|
+
|
|
186
|
+
IF p_kind = 'cron' AND v_inserted = 1 THEN
|
|
187
|
+
UPDATE public.amalgm_automation_triggers SET
|
|
188
|
+
next_run_at = p_next_run_at,
|
|
189
|
+
remaining_occurrences = CASE WHEN remaining_occurrences IS NULL
|
|
190
|
+
THEN NULL ELSE remaining_occurrences - 1 END,
|
|
191
|
+
enabled = CASE WHEN remaining_occurrences = 1 THEN false ELSE enabled END,
|
|
192
|
+
updated_at = p_now
|
|
193
|
+
WHERE user_id = p_user_id AND automation_id = p_automation_id
|
|
194
|
+
AND id = p_trigger_id AND kind = 'schedule'
|
|
195
|
+
AND next_run_at = p_expected_next_run_at;
|
|
196
|
+
END IF;
|
|
197
|
+
END;
|
|
198
|
+
$$;
|
|
199
|
+
|
|
200
|
+
CREATE OR REPLACE FUNCTION public.enqueue_amalgm_manual_run(
|
|
201
|
+
p_user_id uuid,
|
|
202
|
+
p_automation_id text,
|
|
203
|
+
p_input jsonb,
|
|
204
|
+
p_idempotency_key text,
|
|
205
|
+
p_now timestamptz
|
|
206
|
+
) RETURNS SETOF public.amalgm_automation_runs
|
|
207
|
+
LANGUAGE plpgsql
|
|
208
|
+
SECURITY DEFINER
|
|
209
|
+
SET search_path = public
|
|
210
|
+
AS $$
|
|
211
|
+
BEGIN
|
|
212
|
+
IF p_idempotency_key IS NOT NULL THEN
|
|
213
|
+
RETURN QUERY
|
|
214
|
+
SELECT r.*
|
|
215
|
+
FROM public.amalgm_automation_runs r
|
|
216
|
+
WHERE r.user_id = p_user_id
|
|
217
|
+
AND r.automation_id = p_automation_id
|
|
218
|
+
AND r.trigger_id = '@manual'
|
|
219
|
+
AND r.delivery_key = p_idempotency_key;
|
|
220
|
+
IF FOUND THEN RETURN; END IF;
|
|
221
|
+
END IF;
|
|
222
|
+
|
|
223
|
+
RETURN QUERY
|
|
224
|
+
INSERT INTO public.amalgm_automation_runs (
|
|
225
|
+
user_id, target_id, automation_id, trigger_id, workflow_id,
|
|
226
|
+
automation_payload, input, status, delivery_key, created_at
|
|
227
|
+
)
|
|
228
|
+
SELECT a.user_id, a.target_id, a.id, '@manual', w.id,
|
|
229
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
230
|
+
'id', a.id, 'targetId', a.target_id, 'name', a.name,
|
|
231
|
+
'description', a.description, 'enabled', a.enabled,
|
|
232
|
+
'trigger', jsonb_build_object(
|
|
233
|
+
'id', '@manual', 'kind', 'manual', 'enabled', true
|
|
234
|
+
),
|
|
235
|
+
'workflow', jsonb_strip_nulls(jsonb_build_object(
|
|
236
|
+
'id', w.id, 'name', w.name, 'script', w.script,
|
|
237
|
+
'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
|
|
238
|
+
))
|
|
239
|
+
)),
|
|
240
|
+
p_input, 'pending', p_idempotency_key, p_now
|
|
241
|
+
FROM public.amalgm_automations a
|
|
242
|
+
JOIN public.amalgm_automation_workflows w
|
|
243
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
244
|
+
WHERE a.user_id = p_user_id AND a.id = p_automation_id AND a.enabled
|
|
245
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
246
|
+
ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
|
|
247
|
+
WHERE delivery_key IS NOT NULL
|
|
248
|
+
DO UPDATE SET delivery_key = EXCLUDED.delivery_key
|
|
249
|
+
RETURNING *;
|
|
250
|
+
END;
|
|
251
|
+
$$;
|
|
252
|
+
|
|
253
|
+
REVOKE ALL ON FUNCTION public.amalgm_workflow_is_executable(jsonb) FROM PUBLIC;
|
|
254
|
+
REVOKE ALL ON FUNCTION public.list_amalgm_event_triggers(uuid, text) FROM PUBLIC;
|
|
255
|
+
REVOKE ALL ON FUNCTION public.list_due_amalgm_crons(timestamptz) FROM PUBLIC;
|
|
256
|
+
REVOKE ALL ON FUNCTION public.resolve_amalgm_webhook(text) FROM PUBLIC;
|
|
257
|
+
REVOKE ALL ON FUNCTION public.enqueue_amalgm_run(
|
|
258
|
+
text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
|
|
259
|
+
) FROM PUBLIC;
|
|
260
|
+
REVOKE ALL ON FUNCTION public.enqueue_amalgm_manual_run(
|
|
261
|
+
uuid, text, jsonb, text, timestamptz
|
|
262
|
+
) FROM PUBLIC;
|
|
263
|
+
|
|
264
|
+
GRANT EXECUTE ON FUNCTION public.list_amalgm_event_triggers(uuid, text) TO service_role;
|
|
265
|
+
GRANT EXECUTE ON FUNCTION public.list_due_amalgm_crons(timestamptz) TO service_role;
|
|
266
|
+
GRANT EXECUTE ON FUNCTION public.resolve_amalgm_webhook(text) TO service_role;
|
|
267
|
+
GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_run(
|
|
268
|
+
text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
|
|
269
|
+
) TO service_role;
|
|
270
|
+
GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_manual_run(
|
|
271
|
+
uuid, text, jsonb, text, timestamptz
|
|
272
|
+
) TO service_role;
|