pgkiln 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +7 -0
- package/db/migrations/119_workspace_home.sql +30 -0
- package/db/migrations/120_datasets.sql +87 -0
- package/db/migrations/121_ai_agents.sql +94 -0
- package/db/migrations/122_team_development.sql +186 -0
- package/db/migrations/123_workflow_outcomes.sql +407 -0
- package/db/migrations/124_component_details.sql +51 -0
- package/docs/apex-feature-parity.md +15 -12
- package/docs/guide/01-installation.md +9 -3
- package/docs/guide/03-builder.md +211 -11
- package/docs/guide/04-pages-and-regions.md +96 -22
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +123 -8
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/09-reference.md +2 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +70 -11
- package/docs/guide/20-ai-agents.md +4 -2
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_63_datasets.sql +29 -0
- package/examples/hr/hr_64_ai_agent.sql +97 -0
- package/examples/hr/hr_65_ask_reports.sql +21 -0
- package/examples/hr/hr_66_team_development.sql +58 -0
- package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
- package/examples/hr/hr_68_component_details.sql +96 -0
- package/package.json +1 -1
- package/public/app.css +46 -0
- package/public/app.js +134 -20
- package/public/builder.css +116 -0
- package/public/builder.js +47 -0
- package/src/ai/chat.ts +18 -18
- package/src/ai/service.ts +35 -11
- package/src/ai/types.ts +7 -2
- package/src/app.ts +11 -1
- package/src/builder/advisor.ts +6 -3
- package/src/builder/ai-agents.ts +372 -0
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/ai.ts +13 -9
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/datasets.ts +472 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +12 -7
- package/src/builder/instance.ts +2 -0
- package/src/builder/locks.ts +7 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/reporter.ts +52 -3
- package/src/builder/routes.ts +7 -4
- package/src/builder/shared.ts +1 -1
- package/src/builder/subscriptions.ts +1 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/ui.ts +8 -5
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +285 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +7 -4
- package/src/cli/replace.ts +7 -1
- package/src/datasets.ts +367 -0
- package/src/i18n/ar.ts +33 -0
- package/src/i18n/cs.ts +33 -0
- package/src/i18n/da.ts +33 -0
- package/src/i18n/de.ts +33 -0
- package/src/i18n/el.ts +33 -0
- package/src/i18n/es.ts +33 -0
- package/src/i18n/fi.ts +33 -0
- package/src/i18n/fr.ts +33 -0
- package/src/i18n/he.ts +33 -0
- package/src/i18n/it.ts +33 -0
- package/src/i18n/ja.ts +33 -0
- package/src/i18n/ko.ts +33 -0
- package/src/i18n/nb.ts +33 -0
- package/src/i18n/pl.ts +33 -0
- package/src/i18n/pt.ts +33 -0
- package/src/i18n/ru.ts +33 -0
- package/src/i18n/sv.ts +33 -0
- package/src/i18n/tr.ts +33 -0
- package/src/i18n/uk.ts +33 -0
- package/src/i18n/zh.ts +33 -0
- package/src/i18n.ts +66 -0
- package/src/instance.ts +3 -0
- package/src/metadata.ts +9 -3
- package/src/release-check.ts +139 -0
- package/src/reporting-app.ts +101 -0
- package/src/runtime/ai-agents.ts +365 -0
- package/src/runtime/ai-filter.ts +385 -107
- package/src/runtime/assistant.ts +64 -11
- package/src/runtime/context.ts +2 -0
- package/src/runtime/data-reporter.ts +199 -32
- package/src/runtime/ir-phrases.ts +812 -0
- package/src/runtime/ir-plan.ts +449 -0
- package/src/runtime/items.ts +1 -1
- package/src/runtime/regions.ts +6 -2
- package/src/runtime/render.ts +1 -1
- package/src/runtime/report.ts +1 -1
- package/src/runtime/tasks.ts +63 -12
- package/src/session.ts +2 -0
- package/src/subscriptions.ts +2 -1
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +39 -4
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 123: workflows and approvals (APEX 26.1 / 26.2): custom task outcomes,
|
|
3
|
+
-- task events and notifications, draft steps, migrating running instances
|
|
4
|
+
--
|
|
5
|
+
-- Task definitions:
|
|
6
|
+
-- outcomes [{"name": "YES", "label": "Yes", "colour": "success"}, …]: the buttons of the
|
|
7
|
+
-- task and the outcomes a task may end with (stored lower case in meta.task.outcome);
|
|
8
|
+
-- empty or null: the type's own (approval: approved / rejected, action: completed).
|
|
9
|
+
-- Colours: default, primary, success, danger, warning.
|
|
10
|
+
-- events [{"on": "complete" | "expire" | "fail", "type": "sql" | "mail" | "workflow", …}]:
|
|
11
|
+
-- actions when a task is completed, expires, or its completion SQL fails
|
|
12
|
+
-- (src/task-events.ts runs them as the application's role).
|
|
13
|
+
-- fail_recovery after a failing completion SQL: reopen (the decision is undone; the default) or
|
|
14
|
+
-- complete (the decision stands without the SQL's effect).
|
|
15
|
+
-- notifications {"create": "TEMPLATE", "complete": "TEMPLATE", "expire": "TEMPLATE"}: built-in
|
|
16
|
+
-- e-mails from the application's e-mail templates (meta.send_mail_template) to the
|
|
17
|
+
-- potential owners (create) or the initiator (complete; expire: both).
|
|
18
|
+
-- All are columns of meta.task_definition: they travel with export/import as they are.
|
|
19
|
+
--
|
|
20
|
+
-- meta.task_hook: the outbox of task events and notifications (create, complete, expire), filled
|
|
21
|
+
-- by a trigger in the task's transaction and worked off by the server after the commit. Runtime
|
|
22
|
+
-- data like the tasks themselves (no application foreign key: not exported, kept on replace).
|
|
23
|
+
--
|
|
24
|
+
-- Workflows:
|
|
25
|
+
-- a "draft" step type, a placeholder while modelling: allowed in a development version only
|
|
26
|
+
-- (a check on the active steps; activation refuses it);
|
|
27
|
+
-- a "send_mail" step (src/workflow.ts) through meta.send_mail / meta.send_mail_template;
|
|
28
|
+
-- meta.migrate_workflows(): running instances move to a newer version of their definition,
|
|
29
|
+
-- each current step mapped to a step of that version, in one transaction, logged in
|
|
30
|
+
-- meta.workflow_event ("migrated") and meta.workflow_migration (installation data).
|
|
31
|
+
-- =====================================================================
|
|
32
|
+
|
|
33
|
+
-- ---------------------------------------------------------------- outcomes
|
|
34
|
+
/** Why a task definition's outcomes are not valid, or null. */
|
|
35
|
+
create function meta.task_outcomes_problem(p jsonb) returns text
|
|
36
|
+
language plpgsql immutable set search_path = pg_catalog as $$
|
|
37
|
+
declare
|
|
38
|
+
e jsonb;
|
|
39
|
+
v_seen text[] := '{}';
|
|
40
|
+
v_name text;
|
|
41
|
+
begin
|
|
42
|
+
if p is null or p = '[]'::jsonb then
|
|
43
|
+
return null;
|
|
44
|
+
end if;
|
|
45
|
+
if jsonb_typeof(p) <> 'array' then
|
|
46
|
+
return 'Outcomes are a JSON array.';
|
|
47
|
+
end if;
|
|
48
|
+
if jsonb_array_length(p) > 10 then
|
|
49
|
+
return 'At most 10 outcomes.';
|
|
50
|
+
end if;
|
|
51
|
+
for e in select * from jsonb_array_elements(p) loop
|
|
52
|
+
if jsonb_typeof(e) <> 'object' then
|
|
53
|
+
return 'Each outcome is an object {"name": …, "label": …, "colour": …}.';
|
|
54
|
+
end if;
|
|
55
|
+
v_name := lower(e->>'name');
|
|
56
|
+
if v_name is null or v_name !~ '^[a-z][a-z0-9_]{0,29}$' then
|
|
57
|
+
return format('Outcome name %s: letters, digits and underscores (at most 30), e.g. YES.', coalesce(e->>'name', '(none)'));
|
|
58
|
+
end if;
|
|
59
|
+
if v_name in ('cancelled', 'expired', 'default') then
|
|
60
|
+
return format('Outcome name %s is reserved.', e->>'name');
|
|
61
|
+
end if;
|
|
62
|
+
if v_name = any (v_seen) then
|
|
63
|
+
return format('Outcome %s is listed twice.', e->>'name');
|
|
64
|
+
end if;
|
|
65
|
+
v_seen := v_seen || v_name;
|
|
66
|
+
if e ? 'label' and (jsonb_typeof(e->'label') <> 'string' or length(btrim(e->>'label')) not between 1 and 60) then
|
|
67
|
+
return format('Outcome %s: the label is text of 1 to 60 characters.', e->>'name');
|
|
68
|
+
end if;
|
|
69
|
+
if e ? 'colour' and coalesce(e->>'colour', '') not in ('default', 'primary', 'success', 'danger', 'warning') then
|
|
70
|
+
return format('Outcome %s: the colour is default, primary, success, danger or warning.', e->>'name');
|
|
71
|
+
end if;
|
|
72
|
+
end loop;
|
|
73
|
+
return null;
|
|
74
|
+
end
|
|
75
|
+
$$;
|
|
76
|
+
|
|
77
|
+
alter table meta.task_definition
|
|
78
|
+
add column outcomes jsonb constraint task_definition_outcomes_check check (meta.task_outcomes_problem(outcomes) is null),
|
|
79
|
+
add column events jsonb check (events is null or (jsonb_typeof(events) = 'array' and jsonb_array_length(events) <= 20)),
|
|
80
|
+
add column fail_recovery text check (fail_recovery in ('reopen', 'complete')),
|
|
81
|
+
add column notifications jsonb check (notifications is null or (jsonb_typeof(notifications) = 'object'
|
|
82
|
+
and notifications - 'create' - 'complete' - 'expire' = '{}'::jsonb));
|
|
83
|
+
comment on column meta.task_definition.outcomes is 'Custom outcomes (123): [{"name", "label", "colour"}]; empty: the type''s own.';
|
|
84
|
+
comment on column meta.task_definition.events is 'Task events (123): [{"on": complete|expire|fail, "type": sql|mail|workflow, …}].';
|
|
85
|
+
comment on column meta.task_definition.notifications is 'Built-in notifications (123): {"create"|"complete"|"expire": e-mail template name}.';
|
|
86
|
+
|
|
87
|
+
/** The outcomes a task of this definition may end with (lower case). */
|
|
88
|
+
create function meta.task_outcome_names(d meta.task_definition) returns text[]
|
|
89
|
+
language sql immutable set search_path = pg_catalog as $$
|
|
90
|
+
select case when d.outcomes is not null and jsonb_array_length(d.outcomes) > 0
|
|
91
|
+
then array(select lower(e->>'name') from jsonb_array_elements(d.outcomes) e)
|
|
92
|
+
when d.type = 'approval' then array['approved', 'rejected']
|
|
93
|
+
else array['completed'] end
|
|
94
|
+
$$;
|
|
95
|
+
|
|
96
|
+
alter table meta.task drop constraint task_outcome_check;
|
|
97
|
+
alter table meta.task add constraint task_outcome_check check (outcome ~ '^[a-z][a-z0-9_]{0,29}$');
|
|
98
|
+
alter table meta.task_event drop constraint task_event_event_check;
|
|
99
|
+
alter table meta.task_event add constraint task_event_event_check
|
|
100
|
+
check (event in ('created', 'claimed', 'released', 'delegated', 'approved', 'rejected', 'completed', 'cancelled', 'commented', 'closed',
|
|
101
|
+
'expired', 'renewed', 'escalated', 'failed'));
|
|
102
|
+
|
|
103
|
+
-- Same as 021, plus: the outcomes of the definition (custom ones too). A custom outcome is logged as
|
|
104
|
+
-- "completed" with its label. Returns also the outcome, its label, the fail events and the recovery.
|
|
105
|
+
create or replace function meta.complete_task(p_id bigint, p_outcome text, p_comment text default null) returns jsonb
|
|
106
|
+
language plpgsql security definer set search_path = meta, pg_catalog as $$
|
|
107
|
+
declare
|
|
108
|
+
t meta.task;
|
|
109
|
+
d meta.task_definition;
|
|
110
|
+
v_out text := lower(btrim(coalesce(p_outcome, '')));
|
|
111
|
+
v_label text;
|
|
112
|
+
begin
|
|
113
|
+
t := meta.task_load(p_id, 'act');
|
|
114
|
+
select * into d from task_definition where id = t.definition_id;
|
|
115
|
+
if not v_out = any (meta.task_outcome_names(d)) then
|
|
116
|
+
raise exception 'A task of % cannot end as %.', d.name, left(coalesce(p_outcome, '(none)'), 60);
|
|
117
|
+
end if;
|
|
118
|
+
select e->>'label' into v_label from jsonb_array_elements(coalesce(d.outcomes, '[]')) e where lower(e->>'name') = v_out;
|
|
119
|
+
update task set state = 'completed', outcome = v_out, completed_at = now(), completed_by = meta.app_user(),
|
|
120
|
+
actual_owner = coalesce(actual_owner, meta.app_user())
|
|
121
|
+
where id = p_id;
|
|
122
|
+
if v_out in ('approved', 'rejected', 'completed') then
|
|
123
|
+
perform meta.task_log(p_id, v_out, p_comment);
|
|
124
|
+
else
|
|
125
|
+
perform meta.task_log(p_id, 'completed', coalesce(v_label, upper(v_out)) || coalesce(': ' || nullif(btrim(p_comment), ''), ''));
|
|
126
|
+
end if;
|
|
127
|
+
return jsonb_build_object('action_code', d.action_code, 'detail_pk', t.detail_pk, 'params', t.params, 'initiator', t.initiator,
|
|
128
|
+
'outcome', v_out, 'label', coalesce(v_label, v_out), 'subject', t.subject, 'definition', d.name,
|
|
129
|
+
'fail_events', coalesce((select jsonb_agg(e) from jsonb_array_elements(coalesce(d.events, '[]')) e where e->>'on' = 'fail'), '[]'),
|
|
130
|
+
'fail_recovery', coalesce(d.fail_recovery, 'reopen'));
|
|
131
|
+
end
|
|
132
|
+
$$;
|
|
133
|
+
|
|
134
|
+
-- Same as 021, plus: a custom outcome of the definition.
|
|
135
|
+
create or replace function meta.close_tasks(p_name text, p_detail_pk text, p_outcome text default null) returns int
|
|
136
|
+
language plpgsql security definer set search_path = meta, pg_catalog as $$
|
|
137
|
+
declare
|
|
138
|
+
v_n int := 0;
|
|
139
|
+
v_id bigint;
|
|
140
|
+
d meta.task_definition;
|
|
141
|
+
begin
|
|
142
|
+
select * into d from task_definition where app_id = meta.app_id() and name = upper(p_name);
|
|
143
|
+
if p_outcome is not null and not (found and lower(p_outcome) = any (meta.task_outcome_names(d))) then
|
|
144
|
+
raise exception 'Invalid outcome %.', left(p_outcome, 60);
|
|
145
|
+
end if;
|
|
146
|
+
for v_id in
|
|
147
|
+
update task t set state = case when p_outcome is null then 'cancelled' else 'completed' end,
|
|
148
|
+
outcome = lower(p_outcome), completed_at = now(), completed_by = meta.app_user()
|
|
149
|
+
where t.definition_id = d.id and t.detail_pk = p_detail_pk and t.state in ('unassigned', 'assigned')
|
|
150
|
+
returning t.id
|
|
151
|
+
loop
|
|
152
|
+
perform meta.task_log(v_id, 'closed', p_outcome);
|
|
153
|
+
v_n := v_n + 1;
|
|
154
|
+
end loop;
|
|
155
|
+
return v_n;
|
|
156
|
+
end
|
|
157
|
+
$$;
|
|
158
|
+
|
|
159
|
+
-- The completion SQL of a task failed and its fail events ran (the task list, as the acting user).
|
|
160
|
+
create function meta.log_task_failure(p_id bigint, p_detail text) returns void
|
|
161
|
+
language plpgsql security definer set search_path = meta, pg_catalog as $$
|
|
162
|
+
declare
|
|
163
|
+
t meta.task;
|
|
164
|
+
begin
|
|
165
|
+
select * into t from task where id = p_id and app_id = meta.app_id();
|
|
166
|
+
if not found or not ((meta.task_rights(t)).may_act or (t.state = 'completed' and lower(t.completed_by) = lower(meta.app_user()))) then
|
|
167
|
+
raise exception 'Task % not found.', p_id;
|
|
168
|
+
end if;
|
|
169
|
+
perform meta.task_log(p_id, 'failed', left(p_detail, 4000));
|
|
170
|
+
end
|
|
171
|
+
$$;
|
|
172
|
+
revoke all on function meta.log_task_failure(bigint, text) from public;
|
|
173
|
+
grant execute on function meta.log_task_failure(bigint, text) to public;
|
|
174
|
+
|
|
175
|
+
-- ---------------------------------------------------------------- events and notifications: the outbox
|
|
176
|
+
create table meta.task_hook (
|
|
177
|
+
id bigserial primary key,
|
|
178
|
+
task_id bigint not null references meta.task on delete cascade,
|
|
179
|
+
event text not null check (event in ('create', 'complete', 'expire')),
|
|
180
|
+
created_at timestamptz not null default now(),
|
|
181
|
+
attempts int not null default 0,
|
|
182
|
+
done_at timestamptz,
|
|
183
|
+
error text check (length(error) <= 2000)
|
|
184
|
+
);
|
|
185
|
+
create index task_hook_pending on meta.task_hook (id) where done_at is null;
|
|
186
|
+
revoke all on meta.task_hook from public;
|
|
187
|
+
comment on table meta.task_hook is 'Task events and notifications to run after the commit (123); runtime data, not exported.';
|
|
188
|
+
|
|
189
|
+
create function meta.task_hook_enqueue() returns trigger
|
|
190
|
+
language plpgsql security definer set search_path = meta, pg_catalog as $$
|
|
191
|
+
declare
|
|
192
|
+
d meta.task_definition;
|
|
193
|
+
v_event text := case when tg_op = 'INSERT' then 'create' when new.state = 'completed' then 'complete' when new.state = 'expired' then 'expire' end;
|
|
194
|
+
begin
|
|
195
|
+
if v_event is null then
|
|
196
|
+
return new;
|
|
197
|
+
end if;
|
|
198
|
+
select * into d from task_definition where id = new.definition_id;
|
|
199
|
+
if coalesce(d.notifications ? v_event, false)
|
|
200
|
+
or exists (select 1 from jsonb_array_elements(coalesce(d.events, '[]')) e where e->>'on' = v_event) then
|
|
201
|
+
insert into task_hook (task_id, event) values (new.id, v_event);
|
|
202
|
+
perform pg_notify('pgkiln_workflow', 'task');
|
|
203
|
+
end if;
|
|
204
|
+
return new;
|
|
205
|
+
end
|
|
206
|
+
$$;
|
|
207
|
+
create trigger task_hook_create after insert on meta.task for each row execute function meta.task_hook_enqueue();
|
|
208
|
+
create trigger task_hook_state after update of state on meta.task
|
|
209
|
+
for each row when (old.state is distinct from new.state and new.state in ('completed', 'expired')) execute function meta.task_hook_enqueue();
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The e-mail addresses of a task's people (accounts with an address): its potential owners (named users,
|
|
213
|
+
* the actual owner, the application's users with one of its owner roles) and/or its initiator.
|
|
214
|
+
*/
|
|
215
|
+
create function meta.task_addresses(p_task_id bigint, p_owners boolean, p_initiator boolean) returns text[]
|
|
216
|
+
language sql stable security definer set search_path = meta, pg_catalog as $$
|
|
217
|
+
select coalesce(array_agg(distinct ac.email order by ac.email), '{}')
|
|
218
|
+
from task t
|
|
219
|
+
join account ac on ac.active and ac.email is not null
|
|
220
|
+
where t.id = p_task_id
|
|
221
|
+
and ((p_initiator and lower(ac.username) = lower(t.initiator))
|
|
222
|
+
or (p_owners and (lower(ac.username) = any (select lower(u) from unnest(t.owner_users) u)
|
|
223
|
+
or lower(ac.username) = lower(t.actual_owner)
|
|
224
|
+
or exists (select 1 from app_access aa
|
|
225
|
+
where aa.app_id = t.app_id and aa.account_id = ac.id
|
|
226
|
+
and exists (select 1 from unnest(aa.roles) r where lower(r) = any (select lower(x) from unnest(t.owner_roles) x))))))
|
|
227
|
+
$$;
|
|
228
|
+
revoke all on function meta.task_addresses(bigint, boolean, boolean) from public;
|
|
229
|
+
|
|
230
|
+
-- ---------------------------------------------------------------- draft steps
|
|
231
|
+
alter table meta.workflow_definition add constraint workflow_definition_no_draft
|
|
232
|
+
check (not jsonb_path_exists(steps, '$[*] ? (@.type == "draft")'));
|
|
233
|
+
|
|
234
|
+
-- Same as 027, plus: a version with draft steps is not activated.
|
|
235
|
+
create or replace function meta.activate_workflow_version(p_definition_id int) returns text
|
|
236
|
+
language plpgsql set search_path = meta, pg_catalog as $$
|
|
237
|
+
declare
|
|
238
|
+
d meta.workflow_definition;
|
|
239
|
+
v_drafts text;
|
|
240
|
+
begin
|
|
241
|
+
select * into d from workflow_definition where id = p_definition_id for update;
|
|
242
|
+
if not found then
|
|
243
|
+
raise exception 'Workflow definition % does not exist.', p_definition_id;
|
|
244
|
+
end if;
|
|
245
|
+
if d.dev_version is null then
|
|
246
|
+
raise exception 'Workflow % has no version in development.', d.name;
|
|
247
|
+
end if;
|
|
248
|
+
if jsonb_array_length(d.dev_steps) = 0 then
|
|
249
|
+
raise exception 'Version % of workflow % has no steps.', d.dev_version, d.name;
|
|
250
|
+
end if;
|
|
251
|
+
select string_agg(s->>'name', ', ') into v_drafts from jsonb_array_elements(d.dev_steps) s where s->>'type' = 'draft';
|
|
252
|
+
if v_drafts is not null then
|
|
253
|
+
raise exception 'Version % of workflow % has draft steps (%): replace them before activating it.', d.dev_version, d.name, v_drafts;
|
|
254
|
+
end if;
|
|
255
|
+
update workflow_definition
|
|
256
|
+
set inactive_versions = inactive_versions || jsonb_build_array(jsonb_build_object(
|
|
257
|
+
'version', version, 'steps', steps, 'activated_at', activated_at, 'deactivated_at', now())),
|
|
258
|
+
version = dev_version, steps = dev_steps, activated_at = now(),
|
|
259
|
+
dev_version = null, dev_steps = null
|
|
260
|
+
where id = d.id;
|
|
261
|
+
return d.dev_version;
|
|
262
|
+
end
|
|
263
|
+
$$;
|
|
264
|
+
revoke all on function meta.activate_workflow_version(int) from public;
|
|
265
|
+
|
|
266
|
+
-- ---------------------------------------------------------------- migrating running instances
|
|
267
|
+
alter table meta.workflow_event drop constraint workflow_event_event_check;
|
|
268
|
+
alter table meta.workflow_event add constraint workflow_event_event_check
|
|
269
|
+
check (event in ('started', 'step', 'task', 'waiting', 'resumed', 'completed', 'terminated', 'faulted', 'retried', 'split', 'joined', 'cancelled', 'migrated'));
|
|
270
|
+
|
|
271
|
+
create table meta.workflow_migration (
|
|
272
|
+
id bigserial primary key,
|
|
273
|
+
app_id int not null references meta.app on delete cascade,
|
|
274
|
+
definition_name text not null,
|
|
275
|
+
to_version text not null,
|
|
276
|
+
-- current step → step of the new version
|
|
277
|
+
mapping jsonb not null,
|
|
278
|
+
workflow_ids bigint[] not null,
|
|
279
|
+
-- per instance: {"id", "from_version", "from_step", "to_step"}
|
|
280
|
+
detail jsonb not null default '[]',
|
|
281
|
+
migrated_by text not null,
|
|
282
|
+
migrated_at timestamptz not null default now()
|
|
283
|
+
);
|
|
284
|
+
create index on meta.workflow_migration (app_id, migrated_at);
|
|
285
|
+
revoke all on meta.workflow_migration from public;
|
|
286
|
+
comment on table meta.workflow_migration is 'Log of migrations of running workflow instances to a newer version (123); installation data, not exported.';
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Move running instances (p_ids) of a definition to a newer version (p_to_version: the active version or a
|
|
290
|
+
* later inactive one), each at the step p_map gives for its current step. Returns one row per instance with
|
|
291
|
+
* the problem that stops it (null: it can move). With p_apply, nothing changes unless every instance can
|
|
292
|
+
* move; then all move in this transaction, with a "migrated" event each and a row in meta.workflow_migration.
|
|
293
|
+
*/
|
|
294
|
+
create function meta.migrate_workflows(p_definition_id int, p_to_version text, p_ids bigint[], p_map jsonb, p_apply boolean, p_by text)
|
|
295
|
+
returns table (workflow_id bigint, from_version text, from_step text, to_step text, problem text)
|
|
296
|
+
language plpgsql set search_path = meta, pg_catalog as $$
|
|
297
|
+
declare
|
|
298
|
+
d meta.workflow_definition;
|
|
299
|
+
v_order text[];
|
|
300
|
+
v_target int;
|
|
301
|
+
v_steps jsonb;
|
|
302
|
+
v_to jsonb;
|
|
303
|
+
v_from jsonb;
|
|
304
|
+
w record;
|
|
305
|
+
v_rows jsonb := '[]';
|
|
306
|
+
v_bad text[] := '{}';
|
|
307
|
+
v_id bigint;
|
|
308
|
+
r jsonb;
|
|
309
|
+
begin
|
|
310
|
+
select * into d from workflow_definition where id = p_definition_id;
|
|
311
|
+
if not found then
|
|
312
|
+
raise exception 'Workflow definition % does not exist.', p_definition_id;
|
|
313
|
+
end if;
|
|
314
|
+
if coalesce(cardinality(p_ids), 0) = 0 then
|
|
315
|
+
raise exception 'Choose the instances to migrate.';
|
|
316
|
+
end if;
|
|
317
|
+
if jsonb_typeof(coalesce(p_map, '{}')) <> 'object' then
|
|
318
|
+
raise exception 'The step mapping is a JSON object.';
|
|
319
|
+
end if;
|
|
320
|
+
-- versions from the oldest to the active one
|
|
321
|
+
v_order := array(select x->>'version' from jsonb_array_elements(d.inactive_versions) with ordinality o(x, i) order by i) || d.version;
|
|
322
|
+
v_target := array_position(v_order, p_to_version);
|
|
323
|
+
if v_target is null then
|
|
324
|
+
raise exception 'Workflow % has no version % to migrate to (the active version or an inactive one).', d.name, p_to_version;
|
|
325
|
+
end if;
|
|
326
|
+
v_steps := case when p_to_version = d.version then d.steps
|
|
327
|
+
else (select x->'steps' from jsonb_array_elements(d.inactive_versions) x where x->>'version' = p_to_version limit 1) end;
|
|
328
|
+
|
|
329
|
+
foreach v_id in array (select array_agg(distinct x order by x) from unnest(p_ids) x) loop
|
|
330
|
+
select wf.id, wf.app_id, wf.definition_id, coalesce(wf.version, '1') as version, wf.state, wf.current_step, wf.steps, wf.waiting_task, wf.wait_until,
|
|
331
|
+
(select td.name from task tk join task_definition td on td.id = tk.definition_id where tk.id = wf.waiting_task) as task_name,
|
|
332
|
+
exists (select 1 from workflow_branch b where b.workflow_id = wf.id and b.state in ('active', 'waiting', 'faulted')) as branches
|
|
333
|
+
into w from workflow wf where wf.id = v_id for update;
|
|
334
|
+
workflow_id := v_id;
|
|
335
|
+
from_version := w.version;
|
|
336
|
+
from_step := w.current_step;
|
|
337
|
+
to_step := case when w.current_step is not null then p_map->>w.current_step end;
|
|
338
|
+
problem := null;
|
|
339
|
+
v_to := (select s from jsonb_array_elements(v_steps) s where s->>'name' = to_step limit 1);
|
|
340
|
+
v_from := (select s from jsonb_array_elements(w.steps) s where s->>'name' = w.current_step limit 1);
|
|
341
|
+
if w.id is null or w.definition_id is distinct from d.id or w.app_id <> d.app_id then
|
|
342
|
+
problem := 'not an instance of this workflow';
|
|
343
|
+
from_version := null;
|
|
344
|
+
from_step := null;
|
|
345
|
+
to_step := null;
|
|
346
|
+
elsif w.state not in ('active', 'waiting', 'faulted') then
|
|
347
|
+
problem := format('is %s', w.state);
|
|
348
|
+
elsif coalesce(array_position(v_order, w.version), 0) >= v_target then
|
|
349
|
+
problem := format('runs version %s, which is not older than %s', w.version, p_to_version);
|
|
350
|
+
elsif w.branches then
|
|
351
|
+
problem := 'runs parallel branches: let them reach their join first';
|
|
352
|
+
elsif to_step is null or to_step = '' then
|
|
353
|
+
problem := format('no step of version %s is mapped to %s', p_to_version, coalesce(w.current_step, '(none)'));
|
|
354
|
+
elsif v_to is null then
|
|
355
|
+
problem := format('version %s has no step %s', p_to_version, to_step);
|
|
356
|
+
elsif v_to->>'type' = 'draft' then
|
|
357
|
+
problem := format('%s is a draft step', to_step);
|
|
358
|
+
elsif w.waiting_task is not null and v_to->>'type' <> 'task' then
|
|
359
|
+
problem := format('waits for a task: %s must be a task step', to_step);
|
|
360
|
+
elsif w.waiting_task is not null and upper(v_to->>'task') is distinct from w.task_name then
|
|
361
|
+
problem := format('waits for a task of %s, but %s creates %s', w.task_name, to_step, coalesce(v_to->>'task', '(none)'));
|
|
362
|
+
elsif w.state = 'waiting' and w.waiting_task is null and v_from->>'type' = 'invoke_api' then
|
|
363
|
+
problem := 'is calling a web service: try again when the call is done';
|
|
364
|
+
elsif w.state = 'waiting' and w.waiting_task is null and v_from->>'type' = 'wait' and v_to->>'type' <> 'wait' then
|
|
365
|
+
problem := format('waits at a wait step: %s must be a wait step', to_step);
|
|
366
|
+
elsif w.state = 'waiting' and w.waiting_task is null and v_from->>'type' = 'join' and v_to->>'type' <> 'join' then
|
|
367
|
+
problem := format('waits at a join: %s must be a join step', to_step);
|
|
368
|
+
end if;
|
|
369
|
+
if problem is not null then
|
|
370
|
+
v_bad := v_bad || format('%s %s', v_id, problem);
|
|
371
|
+
end if;
|
|
372
|
+
v_rows := v_rows || jsonb_build_object('id', v_id, 'from_version', from_version, 'from_step', from_step, 'to_step', to_step);
|
|
373
|
+
return next;
|
|
374
|
+
end loop;
|
|
375
|
+
|
|
376
|
+
if not p_apply then
|
|
377
|
+
return;
|
|
378
|
+
end if;
|
|
379
|
+
if cardinality(v_bad) > 0 then
|
|
380
|
+
raise exception 'Nothing was migrated: instance %.', array_to_string(v_bad, '; instance ');
|
|
381
|
+
end if;
|
|
382
|
+
for r in select * from jsonb_array_elements(v_rows) loop
|
|
383
|
+
update workflow set steps = v_steps, version = p_to_version, current_step = r->>'to_step', updated_at = now()
|
|
384
|
+
where id = (r->>'id')::bigint;
|
|
385
|
+
perform meta.workflow_log((r->>'id')::bigint, r->>'to_step', 'migrated',
|
|
386
|
+
format('version %s → %s: %s → %s (by %s)', r->>'from_version', p_to_version, r->>'from_step', r->>'to_step', p_by));
|
|
387
|
+
end loop;
|
|
388
|
+
insert into workflow_migration (app_id, definition_name, to_version, mapping, workflow_ids, detail, migrated_by)
|
|
389
|
+
values (d.app_id, d.name, p_to_version, coalesce(p_map, '{}'), array(select (x->>'id')::bigint from jsonb_array_elements(v_rows) x), v_rows, p_by);
|
|
390
|
+
-- active ones go on with their new step
|
|
391
|
+
perform pg_notify('pgkiln_workflow', 'migrated');
|
|
392
|
+
end
|
|
393
|
+
$$;
|
|
394
|
+
revoke all on function meta.migrate_workflows(int, text, bigint[], jsonb, boolean, text) from public;
|
|
395
|
+
|
|
396
|
+
-- Same as 072, plus the definition's custom outcomes (the task list's buttons).
|
|
397
|
+
create or replace view meta.tasks with (security_barrier) as
|
|
398
|
+
select t.id, t.app_id, d.name as definition, d.type, t.subject, t.detail_pk, t.params, t.state, t.outcome,
|
|
399
|
+
t.priority, t.initiator, t.actual_owner, t.owner_users, t.owner_roles, t.created_at, t.due_at,
|
|
400
|
+
t.completed_at, t.completed_by, d.details_page, d.details_item,
|
|
401
|
+
t.due_at is not null and t.due_at < now() and t.state in ('unassigned', 'assigned') as overdue,
|
|
402
|
+
(r).is_initiator, (r).is_owner, (r).is_admin, (r).may_act, (r).may_claim, (r).may_release, (r).may_delegate, (r).may_cancel,
|
|
403
|
+
t.tenant_id, d.outcomes
|
|
404
|
+
from (select t, meta.task_rights(t) as r from meta.task t where t.app_id = meta.app_id()) x
|
|
405
|
+
cross join lateral (select (x.t).*) t
|
|
406
|
+
join meta.task_definition d on d.id = t.definition_id
|
|
407
|
+
where (x.r).may_see;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 124: component details of APEX 26.1 (sprint 115, WS-11)
|
|
3
|
+
--
|
|
4
|
+
-- Dynamic actions:
|
|
5
|
+
-- * event "menu_entry" (APEX: Menu entry chosen): a menu button's entry
|
|
6
|
+
-- {"label": "…", "action": "NAME"} runs the dynamic actions of event
|
|
7
|
+
-- menu_entry whose trigger names the entry (NAME) or the menu button
|
|
8
|
+
-- (any of its entries; the entry's name is the value a client-side
|
|
9
|
+
-- condition tests).
|
|
10
|
+
-- * actions "wait_popup" and "remove_wait_popup" (APEX: Show / Remove
|
|
11
|
+
-- Wait Popup): a modal "please wait" overlay (Message is its text)
|
|
12
|
+
-- until remove_wait_popup or the next page; e.g. before_submit →
|
|
13
|
+
-- wait_popup around a long process, or wait_popup, execute_sql,
|
|
14
|
+
-- remove_wait_popup around an Ajax callback.
|
|
15
|
+
-- Menu entries: exactly one of "page", "request" or "action" (new).
|
|
16
|
+
-- No new tables or columns: the button's menu jsonb and the dynamic
|
|
17
|
+
-- action's event/action travel with export_app / import_app unchanged.
|
|
18
|
+
-- =====================================================================
|
|
19
|
+
|
|
20
|
+
alter table meta.dynamic_action drop constraint dynamic_action_event_check;
|
|
21
|
+
alter table meta.dynamic_action add constraint dynamic_action_event_check check (event in (
|
|
22
|
+
'change', 'click', 'load', 'dialog_closed', 'after_refresh', 'selection_change', 'focus', 'blur', 'key_release', 'before_submit', 'custom',
|
|
23
|
+
'menu_entry'));
|
|
24
|
+
|
|
25
|
+
alter table meta.dynamic_action drop constraint dynamic_action_action_check;
|
|
26
|
+
alter table meta.dynamic_action add constraint dynamic_action_action_check check (action in (
|
|
27
|
+
'show', 'hide', 'enable', 'disable', 'set_value', 'execute_sql', 'refresh_region', 'refresh_item', 'alert', 'submit',
|
|
28
|
+
'set_focus', 'add_class', 'remove_class', 'show_success', 'show_error', 'clear_errors', 'ai_generate', 'execute_javascript', 'plugin',
|
|
29
|
+
'push_subscribe', 'confirm', 'close_dialog', 'cancel_dialog', 'clear', 'open_region', 'close_region', 'download', 'print',
|
|
30
|
+
'current_position', 'share', 'wait_popup', 'remove_wait_popup'));
|
|
31
|
+
|
|
32
|
+
-- A menu entry: {"label": "…", "page": 3, "items": {"P3_ID": "&P2_ID."}} (a link),
|
|
33
|
+
-- {"label": "…", "request": "ARCHIVE"} (submits the page with that request) or
|
|
34
|
+
-- {"label": "…", "action": "EXPORT"} (runs the dynamic actions of event menu_entry);
|
|
35
|
+
-- optional "confirm", "authz" and "icon". At most 20 entries.
|
|
36
|
+
create or replace function meta.button_menu_ok(p_menu jsonb) returns boolean
|
|
37
|
+
language sql immutable set search_path = meta, pg_catalog as $$
|
|
38
|
+
select jsonb_typeof(p_menu) = 'array' and jsonb_array_length(p_menu) <= 20
|
|
39
|
+
and not exists (
|
|
40
|
+
select 1 from jsonb_array_elements(p_menu) e
|
|
41
|
+
where jsonb_typeof(e) <> 'object'
|
|
42
|
+
or jsonb_typeof(e->'label') is distinct from 'string' or length(e->>'label') not between 1 and 100
|
|
43
|
+
or (e ? 'page')::int + (e ? 'request')::int + (e ? 'action')::int <> 1
|
|
44
|
+
or (e ? 'page' and (jsonb_typeof(e->'page') <> 'number' or (e->>'page')::numeric <= 0 or (e->>'page')::numeric <> floor((e->>'page')::numeric)))
|
|
45
|
+
or (e ? 'request' and coalesce(e->>'request', '') !~ '^[A-Z][A-Z0-9_]*$')
|
|
46
|
+
or (e ? 'action' and (jsonb_typeof(e->'action') <> 'string' or e->>'action' !~ '^[A-Z][A-Z0-9_]{0,59}$'))
|
|
47
|
+
or (e ? 'items' and jsonb_typeof(e->'items') <> 'object')
|
|
48
|
+
or (e ? 'confirm' and jsonb_typeof(e->'confirm') <> 'string')
|
|
49
|
+
or (e ? 'icon' and coalesce(e->>'icon', '') !~ '^[a-z][a-z0-9-]{0,40}$')
|
|
50
|
+
or (e ? 'authz' and coalesce(e->>'authz', '') !~ '^!?[A-Z][A-Z0-9_]*$'))
|
|
51
|
+
$$;
|