pgkiln 0.51.0 → 0.53.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/README.md +1 -1
- package/SECURITY.md +10 -1
- 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/db/migrations/125_app_change.sql +23 -0
- package/db/migrations/126_mail_queue_network_access.sql +119 -0
- package/docs/apex-feature-parity.md +50 -19
- package/docs/guide/01-installation.md +8 -4
- package/docs/guide/03-builder.md +197 -10
- package/docs/guide/04-pages-and-regions.md +71 -21
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +129 -9
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/08-security.md +55 -0
- package/docs/guide/09-reference.md +5 -2
- package/docs/guide/11-from-apex.md +3 -1
- package/docs/guide/12-development.md +87 -16
- package/docs/guide/14-globalization.md +49 -1
- package/docs/guide/18-cli.md +21 -1
- package/docs/guide/19-rest-data-sources.md +12 -0
- package/docs/guide/20-ai-agents.md +12 -4
- package/examples/hr/README.md +2 -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/examples/hr/hr_70_data_security.sql +79 -0
- package/package.json +1 -1
- package/public/app.css +135 -21
- package/public/app.js +134 -20
- package/public/builder.css +133 -1
- package/public/builder.js +51 -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 +15 -0
- 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/app-updates.ts +55 -0
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/data-security.ts +182 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +16 -5
- package/src/builder/locks.ts +7 -1
- package/src/builder/mail-queue.ts +212 -0
- package/src/builder/mail.ts +72 -45
- package/src/builder/network-access.ts +140 -0
- package/src/builder/object-actions.ts +1 -0
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/rest-catalogs.ts +19 -4
- package/src/builder/routes.ts +4 -1
- package/src/builder/shared.ts +1 -1
- package/src/builder/sql.ts +2 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/themeroller.ts +47 -6
- package/src/builder/ui.ts +7 -1
- package/src/builder/websources.ts +1 -1
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +2 -0
- package/src/builder/workspace-mail.ts +121 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +1 -0
- package/src/cli/main.ts +25 -3
- package/src/cli/mcp.ts +40 -4
- package/src/cli/only.ts +52 -0
- package/src/cli/replace.ts +11 -1
- package/src/datasec.ts +343 -0
- package/src/i18n/ar.ts +17 -0
- package/src/i18n/cs.ts +17 -0
- package/src/i18n/da.ts +17 -0
- package/src/i18n/de.ts +17 -0
- package/src/i18n/el.ts +17 -0
- package/src/i18n/es.ts +17 -0
- package/src/i18n/fi.ts +17 -0
- package/src/i18n/fr.ts +17 -0
- package/src/i18n/he.ts +17 -0
- package/src/i18n/it.ts +17 -0
- package/src/i18n/ja.ts +17 -0
- package/src/i18n/ko.ts +17 -0
- package/src/i18n/nb.ts +17 -0
- package/src/i18n/pl.ts +17 -0
- package/src/i18n/pt.ts +17 -0
- package/src/i18n/ru.ts +17 -0
- package/src/i18n/sv.ts +17 -0
- package/src/i18n/tr.ts +17 -0
- package/src/i18n/uk.ts +17 -0
- package/src/i18n/zh.ts +17 -0
- package/src/i18n.ts +34 -0
- package/src/instance.ts +2 -0
- package/src/mail/mail.ts +50 -11
- package/src/mail/providers.ts +6 -3
- package/src/metadata.ts +11 -3
- package/src/network-acl.ts +126 -0
- package/src/objectstore.ts +3 -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/engine.ts +3 -1
- 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 +5 -3
- package/src/runtime/report.ts +1 -1
- package/src/runtime/styles.ts +54 -12
- package/src/runtime/tasks.ts +63 -12
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/webclient.ts +45 -4
- package/src/webrequests.ts +5 -0
- package/src/websources.ts +30 -12
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +16 -1
|
@@ -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
|
+
$$;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 125: who changed an application last (sprint 116, WS-12)
|
|
3
|
+
--
|
|
4
|
+
-- APEX's Applications report shows Updated By next to Updated. The builder
|
|
5
|
+
-- records the developer and the time of the last successful change to an
|
|
6
|
+
-- application (any builder POST under /builder/apps/:id or /pages/:pid,
|
|
7
|
+
-- except locks, comments and other actions that change nothing; and the
|
|
8
|
+
-- application a Create or Import made) in meta.app_change: one row per
|
|
9
|
+
-- application. The App Builder list shows it, with the number of working
|
|
10
|
+
-- copies (meta.working_copy) and the application lock (meta.builder_lock).
|
|
11
|
+
--
|
|
12
|
+
-- Builder state of this installation, like meta.builder_lock: written by the
|
|
13
|
+
-- builder as the owner, never read by the runtime; not part of an
|
|
14
|
+
-- application's export (kept by pgkiln import --replace). Not a column of
|
|
15
|
+
-- meta.app on purpose: export_app exports meta.app's columns.
|
|
16
|
+
-- =====================================================================
|
|
17
|
+
|
|
18
|
+
create table meta.app_change (
|
|
19
|
+
app_id int primary key references meta.app on delete cascade,
|
|
20
|
+
updated_by text not null check (length(updated_by) between 1 and 200),
|
|
21
|
+
updated_at timestamptz not null default now()
|
|
22
|
+
);
|
|
23
|
+
comment on table meta.app_change is 'builder: the developer who changed an application last, and when (App Builder list: Updated by)';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 126: Administration 26.1 / 26.2 (sprint 118, WS-9): the mail queue,
|
|
3
|
+
-- a workspace's own mail service and its network access list
|
|
4
|
+
--
|
|
5
|
+
-- meta.workspace.network_restricted / network_hosts: a workspace's
|
|
6
|
+
-- outbound allow-list (APEX 26.2: workspace-level network ACLs). When
|
|
7
|
+
-- restricted, every outgoing web call made for one of its applications
|
|
8
|
+
-- (meta.web_request, REST data sources, invoke API, web credential token
|
|
9
|
+
-- endpoints, object storage, REST source catalogs) must match an entry
|
|
10
|
+
-- here AND the installation's list (PGKILN_REST_ALLOWED_HOSTS /
|
|
11
|
+
-- PGKILN_REST_PRIVATE_HOSTS): the workspace's list only narrows. Entries:
|
|
12
|
+
-- "api.example.com", "*.example.com" (subdomains), "host:8443" (one
|
|
13
|
+
-- port). Restricted with no entries: no outgoing calls at all. Checked in
|
|
14
|
+
-- src/webclient.ts on every hop (redirects too); installation data like
|
|
15
|
+
-- isolation (not in the workspace export).
|
|
16
|
+
-- meta.mail_service.workspace_id: a workspace's own mail service (APEX
|
|
17
|
+
-- 26.1: workspace-level SMTP credentials), managed by the workspace's
|
|
18
|
+
-- administrators; null = an installation service (administrators). Its
|
|
19
|
+
-- secret is encrypted like a web credential's and never shown or
|
|
20
|
+
-- exported. Applications of the workspace without a service of their own
|
|
21
|
+
-- send through it. Its SMTP host is checked like a web request's host.
|
|
22
|
+
-- meta.mail_queue.last_attempt_at / duration_ms / provider: when the last
|
|
23
|
+
-- attempt to hand a mail over ended, how long the provider took, and which
|
|
24
|
+
-- provider it was (Administration → Mail queue).
|
|
25
|
+
-- meta.network_refusal: outgoing calls refused by the allow-lists, per
|
|
26
|
+
-- workspace and application (Administration → Network access), kept 7
|
|
27
|
+
-- days. Installation data: not exported, kept by an in-place import.
|
|
28
|
+
-- =====================================================================
|
|
29
|
+
|
|
30
|
+
-- one entry of a network access list: host, *.domain, host:port, [IPv6]:port (lower case)
|
|
31
|
+
create function meta.network_host_ok(p text) returns boolean
|
|
32
|
+
language sql immutable set search_path = pg_catalog as $$
|
|
33
|
+
select p is not null and length(p) <= 260 and (
|
|
34
|
+
p ~ '^(\*\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*(:[1-9][0-9]{0,4})?$'
|
|
35
|
+
or p ~ '^\[[0-9a-f:.]{2,45}\](:[1-9][0-9]{0,4})?$')
|
|
36
|
+
and coalesce(nullif(substring(p from ':([0-9]+)$'), '')::int, 1) between 1 and 65535
|
|
37
|
+
$$;
|
|
38
|
+
|
|
39
|
+
create function meta.network_hosts_ok(p text[]) returns boolean
|
|
40
|
+
language sql immutable set search_path = pg_catalog as $$
|
|
41
|
+
select p is not null and cardinality(p) <= 200 and coalesce(bool_and(meta.network_host_ok(h)), true) from unnest(p) h
|
|
42
|
+
$$;
|
|
43
|
+
revoke all on function meta.network_host_ok(text), meta.network_hosts_ok(text[]) from public;
|
|
44
|
+
|
|
45
|
+
alter table meta.workspace
|
|
46
|
+
add column network_restricted boolean not null default false,
|
|
47
|
+
add column network_hosts text[] not null default '{}' constraint workspace_network_hosts_ok check (meta.network_hosts_ok(network_hosts));
|
|
48
|
+
comment on column meta.workspace.network_restricted is 'Outgoing web calls of the workspace''s applications are limited to network_hosts (126), within the installation''s list';
|
|
49
|
+
comment on column meta.workspace.network_hosts is 'The workspace''s network access list (126): host, *.domain or host:port entries';
|
|
50
|
+
|
|
51
|
+
-- a workspace's own mail service (one at most); null: an installation service
|
|
52
|
+
alter table meta.mail_service add column workspace_id int references meta.workspace on delete cascade;
|
|
53
|
+
create unique index mail_service_workspace_key on meta.mail_service (workspace_id) where workspace_id is not null;
|
|
54
|
+
comment on column meta.mail_service.workspace_id is 'A workspace''s own mail service (126), managed by its administrators; null = the installation''s';
|
|
55
|
+
|
|
56
|
+
alter table meta.mail_queue
|
|
57
|
+
add column last_attempt_at timestamptz,
|
|
58
|
+
add column duration_ms int,
|
|
59
|
+
add column provider text;
|
|
60
|
+
create index mail_queue_status_attempt on meta.mail_queue (status, last_attempt_at);
|
|
61
|
+
comment on column meta.mail_queue.duration_ms is 'How long the provider took to accept or refuse the last attempt (126)';
|
|
62
|
+
|
|
63
|
+
-- The mail service an application sends through: the one an administrator gave it (an installation
|
|
64
|
+
-- service, or its own workspace's), else its workspace's own service. Null: none.
|
|
65
|
+
create function meta.mail_service_for_app(p_app_id int) returns int
|
|
66
|
+
language sql stable security definer set search_path = meta, pg_catalog as $$
|
|
67
|
+
select coalesce(
|
|
68
|
+
(select x.service_id from meta.app_mail_service x join meta.mail_service s on s.id = x.service_id
|
|
69
|
+
where x.app_id = p_app_id and (s.workspace_id is null or s.workspace_id = meta.app_workspace(p_app_id))),
|
|
70
|
+
case when not exists (select 1 from meta.app_mail_service x where x.app_id = p_app_id)
|
|
71
|
+
then (select s.id from meta.mail_service s where s.workspace_id = meta.app_workspace(p_app_id)) end)
|
|
72
|
+
$$;
|
|
73
|
+
revoke all on function meta.mail_service_for_app(int) from public;
|
|
74
|
+
|
|
75
|
+
-- (081) the calling application and its mail service; now also its workspace's own service
|
|
76
|
+
create or replace function meta.mail_caller(out app_id int, out service_id int, out from_address text, out max_per_day int, out senders text[])
|
|
77
|
+
language plpgsql stable security definer set search_path = meta, pg_catalog as $$
|
|
78
|
+
declare
|
|
79
|
+
v_role text;
|
|
80
|
+
begin
|
|
81
|
+
app_id := meta.app_id();
|
|
82
|
+
if app_id is null then
|
|
83
|
+
raise exception 'meta.send_mail: no current application (call it from application code)';
|
|
84
|
+
end if;
|
|
85
|
+
select a.db_role into v_role from meta.app a where a.id = app_id;
|
|
86
|
+
if not found then
|
|
87
|
+
raise exception 'meta.send_mail: no current application (call it from application code)';
|
|
88
|
+
end if;
|
|
89
|
+
if v_role is not null and current_setting('role') is distinct from v_role then
|
|
90
|
+
raise exception 'meta.send_mail: called outside the application''s own database role';
|
|
91
|
+
end if;
|
|
92
|
+
select s.id, coalesce(x.from_address, nullif(s.settings->>'default_from', '')), x.max_per_day, s.senders
|
|
93
|
+
into service_id, from_address, max_per_day, senders
|
|
94
|
+
from meta.mail_service s left join meta.app_mail_service x on x.app_id = mail_caller.app_id and x.service_id = s.id
|
|
95
|
+
where s.id = meta.mail_service_for_app(mail_caller.app_id) and s.enabled;
|
|
96
|
+
if not found then
|
|
97
|
+
raise exception 'meta.send_mail: this application may not send e-mail (an administrator chooses its mail service under Workspace utilities → Mail services, or the workspace''s administrators set up the workspace''s own)';
|
|
98
|
+
end if;
|
|
99
|
+
end
|
|
100
|
+
$$;
|
|
101
|
+
revoke all on function meta.mail_caller() from public;
|
|
102
|
+
|
|
103
|
+
create table meta.network_refusal (
|
|
104
|
+
id bigint generated always as identity primary key,
|
|
105
|
+
at timestamptz not null default now(),
|
|
106
|
+
workspace_id int references meta.workspace on delete cascade,
|
|
107
|
+
app_id int references meta.app on delete cascade,
|
|
108
|
+
-- what made the call: web request, REST data source, invoke API, token endpoint, object storage, REST catalog, mail
|
|
109
|
+
what text not null check (length(what) <= 100),
|
|
110
|
+
-- host[:port] and the URL without its query string (it may carry keys)
|
|
111
|
+
host text not null check (length(host) <= 300),
|
|
112
|
+
url text check (length(url) <= 1000),
|
|
113
|
+
-- installation (not on the installation's list) or workspace (not on the workspace's)
|
|
114
|
+
list text not null check (list in ('installation', 'workspace')),
|
|
115
|
+
message text check (length(message) <= 1000)
|
|
116
|
+
);
|
|
117
|
+
create index network_refusal_ws on meta.network_refusal (workspace_id, at desc);
|
|
118
|
+
revoke all on meta.network_refusal from public;
|
|
119
|
+
comment on table meta.network_refusal is 'Outgoing web calls refused by the network access lists (126), kept 7 days; installation data, not exported';
|