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.
Files changed (104) hide show
  1. package/SECURITY.md +7 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/db/migrations/121_ai_agents.sql +94 -0
  5. package/db/migrations/122_team_development.sql +186 -0
  6. package/db/migrations/123_workflow_outcomes.sql +407 -0
  7. package/db/migrations/124_component_details.sql +51 -0
  8. package/docs/apex-feature-parity.md +15 -12
  9. package/docs/guide/01-installation.md +9 -3
  10. package/docs/guide/03-builder.md +211 -11
  11. package/docs/guide/04-pages-and-regions.md +96 -22
  12. package/docs/guide/05-items.md +7 -1
  13. package/docs/guide/06-processing.md +123 -8
  14. package/docs/guide/07-dynamic-actions.md +34 -0
  15. package/docs/guide/09-reference.md +2 -1
  16. package/docs/guide/11-from-apex.md +2 -1
  17. package/docs/guide/12-development.md +70 -11
  18. package/docs/guide/20-ai-agents.md +4 -2
  19. package/examples/hr/README.md +2 -0
  20. package/examples/hr/hr_63_datasets.sql +29 -0
  21. package/examples/hr/hr_64_ai_agent.sql +97 -0
  22. package/examples/hr/hr_65_ask_reports.sql +21 -0
  23. package/examples/hr/hr_66_team_development.sql +58 -0
  24. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  25. package/examples/hr/hr_68_component_details.sql +96 -0
  26. package/package.json +1 -1
  27. package/public/app.css +46 -0
  28. package/public/app.js +134 -20
  29. package/public/builder.css +116 -0
  30. package/public/builder.js +47 -0
  31. package/src/ai/chat.ts +18 -18
  32. package/src/ai/service.ts +35 -11
  33. package/src/ai/types.ts +7 -2
  34. package/src/app.ts +11 -1
  35. package/src/builder/advisor.ts +6 -3
  36. package/src/builder/ai-agents.ts +372 -0
  37. package/src/builder/ai-builder.ts +3 -3
  38. package/src/builder/ai.ts +13 -9
  39. package/src/builder/assistant.ts +22 -7
  40. package/src/builder/components.ts +38 -9
  41. package/src/builder/datasets.ts +472 -0
  42. package/src/builder/designer.ts +8 -3
  43. package/src/builder/feedback.ts +6 -1
  44. package/src/builder/home.ts +12 -7
  45. package/src/builder/instance.ts +2 -0
  46. package/src/builder/locks.ts +7 -1
  47. package/src/builder/region-help.ts +2 -2
  48. package/src/builder/reporter.ts +52 -3
  49. package/src/builder/routes.ts +7 -4
  50. package/src/builder/shared.ts +1 -1
  51. package/src/builder/subscriptions.ts +1 -0
  52. package/src/builder/team.ts +991 -0
  53. package/src/builder/ui.ts +8 -5
  54. package/src/builder/workflow-migrate.ts +210 -0
  55. package/src/builder/workflows.ts +29 -6
  56. package/src/builder/workspace-home.ts +285 -0
  57. package/src/builder/workspace-transfer.ts +1 -1
  58. package/src/builder/workspaces.ts +7 -4
  59. package/src/cli/replace.ts +7 -1
  60. package/src/datasets.ts +367 -0
  61. package/src/i18n/ar.ts +33 -0
  62. package/src/i18n/cs.ts +33 -0
  63. package/src/i18n/da.ts +33 -0
  64. package/src/i18n/de.ts +33 -0
  65. package/src/i18n/el.ts +33 -0
  66. package/src/i18n/es.ts +33 -0
  67. package/src/i18n/fi.ts +33 -0
  68. package/src/i18n/fr.ts +33 -0
  69. package/src/i18n/he.ts +33 -0
  70. package/src/i18n/it.ts +33 -0
  71. package/src/i18n/ja.ts +33 -0
  72. package/src/i18n/ko.ts +33 -0
  73. package/src/i18n/nb.ts +33 -0
  74. package/src/i18n/pl.ts +33 -0
  75. package/src/i18n/pt.ts +33 -0
  76. package/src/i18n/ru.ts +33 -0
  77. package/src/i18n/sv.ts +33 -0
  78. package/src/i18n/tr.ts +33 -0
  79. package/src/i18n/uk.ts +33 -0
  80. package/src/i18n/zh.ts +33 -0
  81. package/src/i18n.ts +66 -0
  82. package/src/instance.ts +3 -0
  83. package/src/metadata.ts +9 -3
  84. package/src/release-check.ts +139 -0
  85. package/src/reporting-app.ts +101 -0
  86. package/src/runtime/ai-agents.ts +365 -0
  87. package/src/runtime/ai-filter.ts +385 -107
  88. package/src/runtime/assistant.ts +64 -11
  89. package/src/runtime/context.ts +2 -0
  90. package/src/runtime/data-reporter.ts +199 -32
  91. package/src/runtime/ir-phrases.ts +812 -0
  92. package/src/runtime/ir-plan.ts +449 -0
  93. package/src/runtime/items.ts +1 -1
  94. package/src/runtime/regions.ts +6 -2
  95. package/src/runtime/render.ts +1 -1
  96. package/src/runtime/report.ts +1 -1
  97. package/src/runtime/tasks.ts +63 -12
  98. package/src/session.ts +2 -0
  99. package/src/subscriptions.ts +2 -1
  100. package/src/task-events.ts +232 -0
  101. package/src/team-samples.ts +74 -0
  102. package/src/team.ts +342 -0
  103. package/src/workflow.ts +135 -18
  104. 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
+ $$;