pgkiln 0.70.0 → 0.72.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/db/migrations/137_create_page_types.sql +904 -0
- package/db/migrations/138_app_utilities.sql +153 -0
- package/db/migrations/139_opentelemetry.sql +36 -0
- package/docs/apex-feature-parity.md +9 -7
- package/docs/guide/01-installation.md +108 -0
- package/docs/guide/03-builder.md +116 -4
- package/docs/guide/04-pages-and-regions.md +3 -1
- package/docs/guide/05-items.md +1 -1
- package/docs/guide/06-processing.md +18 -0
- package/docs/guide/09-reference.md +18 -1
- package/docs/guide/12-development.md +35 -1
- package/docs/guide/16-files.md +1 -1
- package/examples/hr/hr_73_configuration_options.sql +17 -0
- package/examples/hr/hr_74_app_utilities.sql +22 -0
- package/package.json +1 -1
- package/public/app.css +9 -0
- package/public/app.js +50 -0
- package/public/builder.css +48 -0
- package/src/ai/service.ts +14 -0
- package/src/app.ts +20 -0
- package/src/builder/advisor.ts +2 -2
- package/src/builder/app-updates.ts +3 -0
- package/src/builder/components.ts +3 -1
- package/src/builder/create-page.ts +259 -0
- package/src/builder/dependencies.ts +311 -0
- package/src/builder/features.ts +313 -0
- package/src/builder/home.ts +3 -0
- package/src/builder/i18n/de.ts +6 -0
- package/src/builder/i18n/en.ts +6 -0
- package/src/builder/i18n/es.ts +6 -0
- package/src/builder/i18n/fr.ts +6 -0
- package/src/builder/i18n/it.ts +6 -0
- package/src/builder/i18n/ja.ts +6 -0
- package/src/builder/i18n/ko.ts +6 -0
- package/src/builder/i18n/nl.ts +6 -0
- package/src/builder/i18n/pt.ts +6 -0
- package/src/builder/i18n/zh-cn.ts +6 -0
- package/src/builder/i18n/zh-tw.ts +6 -0
- package/src/builder/login-page.ts +17 -14
- package/src/builder/newapp.ts +41 -23
- package/src/builder/routes.ts +13 -2
- package/src/builder/telemetry.ts +206 -0
- package/src/builder/ui-defaults.ts +161 -0
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +154 -0
- package/src/builder/utilities.ts +181 -0
- package/src/builder/wizards.ts +211 -26
- package/src/builder/workspaces.ts +2 -1
- package/src/cli/replace.ts +9 -1
- package/src/history.ts +155 -0
- package/src/instance-checks.ts +11 -4
- package/src/layout.ts +2 -1
- package/src/mail/mail.ts +14 -0
- package/src/runtime/ai-agents.ts +9 -0
- package/src/runtime/builtin-components.ts +23 -2
- package/src/runtime/items.ts +4 -2
- package/src/runtime/routes.ts +3 -0
- package/src/session.ts +26 -0
- package/src/telemetry/browser.ts +91 -0
- package/src/telemetry/config.ts +342 -0
- package/src/telemetry/export.ts +484 -0
- package/src/telemetry/http.ts +126 -0
- package/src/telemetry/trace.ts +456 -0
- package/src/webclient.ts +25 -0
- package/src/workflow.ts +14 -7
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 137: Create Page like APEX's (sprint 137, parity card AP-3)
|
|
3
|
+
--
|
|
4
|
+
-- 1. More page types in meta.generate_page (the Create Page grid's
|
|
5
|
+
-- Component tab; APEX: Create a Page → Component):
|
|
6
|
+
-- - on a table or view: report (an interactive report alone), smart_filters
|
|
7
|
+
-- (a report with smart filters above it), tree (id / parent / label, the
|
|
8
|
+
-- parent proposed from a self-referencing foreign key), timeline,
|
|
9
|
+
-- media_list, content_row, comments (the built-in template components
|
|
10
|
+
-- ut_timeline, ut_media_list, ut_content_row, ut_comments), data_load (a
|
|
11
|
+
-- data load definition and a page with a file item, a Load button and a
|
|
12
|
+
-- data_load process) and wizard (a page per step with a wizard progress
|
|
13
|
+
-- list, Previous / Next and an INSERT on Finish);
|
|
14
|
+
-- - without a table: search (a search region over chosen search
|
|
15
|
+
-- configurations, or a new configuration on a table), tasks (a unified
|
|
16
|
+
-- task list) and workflows (the workflow console).
|
|
17
|
+
-- The page types of 047–100 are unchanged: the new meta.generate_page hands
|
|
18
|
+
-- them to the former definition, renamed meta.generate_page_100;
|
|
19
|
+
-- meta.wizard_defaults likewise (meta.wizard_defaults_097). A dashboard is
|
|
20
|
+
-- made by the builder (it reads the data to choose its charts).
|
|
21
|
+
--
|
|
22
|
+
-- 2. Configuration options (the Configuration Options feature): build options
|
|
23
|
+
-- marked configurable (meta.build_option.configurable, exported with the
|
|
24
|
+
-- application) can be switched by the application's administrators at
|
|
25
|
+
-- run time: meta.configurable_options() and
|
|
26
|
+
-- meta.set_configurable_options(names), security definer, only as the
|
|
27
|
+
-- application's own role, for a user with the admin role, and only for
|
|
28
|
+
-- configurable options.
|
|
29
|
+
--
|
|
30
|
+
-- 3. E-mail reporting (the Email Reporting feature):
|
|
31
|
+
-- meta.send_activity_report(to, days) queues a summary of the current
|
|
32
|
+
-- application's activity with the rows as a CSV attachment; for the
|
|
33
|
+
-- application's automations and its administrators.
|
|
34
|
+
--
|
|
35
|
+
-- 4. Create page as copy: meta.page_shared_refs(page id, target app) lists the
|
|
36
|
+
-- shared components a page refers to by name; meta.copy_page(...) copies a
|
|
37
|
+
-- page (regions, items, buttons, dynamic actions, validations, processes,
|
|
38
|
+
-- computations, branches) into the same or another application under a new
|
|
39
|
+
-- number, renaming P<old>_ items to P<new>_, with the shared components
|
|
40
|
+
-- mapped to the target's or copied. Owner only (the builder checks the
|
|
41
|
+
-- developer's workspaces).
|
|
42
|
+
-- =====================================================================
|
|
43
|
+
|
|
44
|
+
-- ---------------------------------------------------------------- 1. page types
|
|
45
|
+
alter function meta.generate_page(text, text, regclass, int, jsonb) rename to generate_page_100;
|
|
46
|
+
alter function meta.wizard_defaults(text, regclass) rename to wizard_defaults_097;
|
|
47
|
+
|
|
48
|
+
-- the application's own data: not pgkiln's or the system's tables (as 047's generator checks)
|
|
49
|
+
create function meta.wizard_check_table(p_table regclass) returns void
|
|
50
|
+
language plpgsql stable as $$
|
|
51
|
+
begin
|
|
52
|
+
if p_table is null then
|
|
53
|
+
raise exception 'choose a table or view';
|
|
54
|
+
end if;
|
|
55
|
+
if (select n.nspname ~ '^pg_' or n.nspname in ('information_schema', 'meta')
|
|
56
|
+
from pg_class rel join pg_namespace n on n.oid = rel.relnamespace where rel.oid = p_table) then
|
|
57
|
+
raise exception 'pages can''t be generated on %', p_table;
|
|
58
|
+
end if;
|
|
59
|
+
if (select relkind from pg_class where oid = p_table) not in ('r', 'p', 'v', 'm') then
|
|
60
|
+
raise exception '% is not a table or view', p_table;
|
|
61
|
+
end if;
|
|
62
|
+
end
|
|
63
|
+
$$;
|
|
64
|
+
|
|
65
|
+
create function meta.wizard_defaults(p_kind text, p_table regclass) returns jsonb
|
|
66
|
+
language plpgsql stable as $$
|
|
67
|
+
declare
|
|
68
|
+
v_pk text;
|
|
69
|
+
v_display text;
|
|
70
|
+
v_skip text[];
|
|
71
|
+
v_fk text;
|
|
72
|
+
v_date text;
|
|
73
|
+
v_text text;
|
|
74
|
+
v_n int;
|
|
75
|
+
v_out jsonb;
|
|
76
|
+
begin
|
|
77
|
+
if p_kind in ('report_form', 'grid', 'form', 'cards', 'calendar', 'chart', 'map', 'facets', 'master_detail') then
|
|
78
|
+
return meta.wizard_defaults_097(p_kind, p_table);
|
|
79
|
+
end if;
|
|
80
|
+
if p_kind in ('search', 'tasks', 'workflows', 'dashboard') then
|
|
81
|
+
v_out := jsonb_build_object('nav', true,
|
|
82
|
+
'label', case p_kind when 'search' then 'Search' when 'tasks' then 'My Tasks' when 'workflows' then 'Workflows' else 'Dashboard' end,
|
|
83
|
+
'icon', case p_kind when 'search' then 'search' when 'tasks' then 'check-circle' when 'workflows' then 'route' else 'dashboard' end);
|
|
84
|
+
return v_out || case p_kind
|
|
85
|
+
when 'search' then jsonb_build_object('configs', '[]'::jsonb, 'form_page', null)
|
|
86
|
+
when 'tasks' then jsonb_build_object('context', 'my', 'completed', false)
|
|
87
|
+
when 'workflows' then jsonb_build_object('context', 'initiator')
|
|
88
|
+
else '{}'::jsonb end;
|
|
89
|
+
end if;
|
|
90
|
+
if p_kind not in ('report', 'smart_filters', 'tree', 'timeline', 'media_list', 'content_row', 'comments', 'data_load', 'wizard') then
|
|
91
|
+
raise exception 'unknown page type "%"', p_kind;
|
|
92
|
+
end if;
|
|
93
|
+
perform meta.wizard_check_table(p_table);
|
|
94
|
+
v_pk := meta.wizard_pk(p_table);
|
|
95
|
+
v_display := meta.wizard_display(p_table);
|
|
96
|
+
v_skip := meta.wizard_sidecars(p_table);
|
|
97
|
+
-- the first foreign key to another table
|
|
98
|
+
select column_name into v_fk from meta.wizard_catalog(p_table)
|
|
99
|
+
where fk_table is not null and not is_pk and fk_table <> p_table order by ordinal limit 1;
|
|
100
|
+
-- a date: one named like a date or a time of an event first
|
|
101
|
+
select column_name into v_date from meta.wizard_catalog(p_table)
|
|
102
|
+
where kind in ('date', 'timestamp') and not (column_name = any (v_skip))
|
|
103
|
+
order by (column_name ~ '(date|_at$|_on$|when|time|created|posted)') desc, ordinal limit 1;
|
|
104
|
+
-- a longer text: a comment, body, note or description first
|
|
105
|
+
select column_name into v_text from meta.wizard_catalog(p_table)
|
|
106
|
+
where kind = 'text' and not is_pk and fk_table is null and column_name is distinct from v_display and not (column_name = any (v_skip))
|
|
107
|
+
order by (column_name ~ '(comment|body|text|note|descr|summary|message|remark|detail)') desc, ordinal limit 1;
|
|
108
|
+
|
|
109
|
+
v_out := jsonb_build_object('nav', p_kind <> 'wizard',
|
|
110
|
+
'label', case p_kind
|
|
111
|
+
when 'wizard' then 'Create ' || meta.wizard_table_label(p_table, false)
|
|
112
|
+
when 'data_load' then 'Load ' || meta.wizard_table_label(p_table)
|
|
113
|
+
else meta.wizard_table_label(p_table) end,
|
|
114
|
+
'icon', case p_kind when 'smart_filters' then 'filter' when 'tree' then 'org' when 'timeline' then 'clock'
|
|
115
|
+
when 'media_list' then 'image' when 'content_row' then 'list' when 'comments' then 'comments'
|
|
116
|
+
when 'data_load' then 'upload' when 'wizard' then 'file' else 'table' end);
|
|
117
|
+
case p_kind
|
|
118
|
+
when 'report' then
|
|
119
|
+
v_out := v_out || jsonb_build_object('form_page', null);
|
|
120
|
+
when 'smart_filters' then
|
|
121
|
+
v_out := v_out || jsonb_build_object('form_page', null, 'facets', coalesce(meta.wizard_defaults_097('facets', p_table) -> 'facets', '[]'));
|
|
122
|
+
when 'tree' then
|
|
123
|
+
v_out := v_out || jsonb_build_object('form_page', null, 'expanded', 1,
|
|
124
|
+
'id_column', v_pk,
|
|
125
|
+
-- the parent: a column with a foreign key to the table itself (mgr → empno)
|
|
126
|
+
'parent_column', (select column_name from meta.wizard_catalog(p_table) where fk_table = p_table and not is_pk order by ordinal limit 1),
|
|
127
|
+
'label_column', coalesce(v_display, v_pk));
|
|
128
|
+
when 'timeline' then
|
|
129
|
+
v_out := v_out || jsonb_build_object('form_page', null, 'title', coalesce(v_display, v_pk), 'when', v_date,
|
|
130
|
+
'who', coalesce((select column_name from meta.wizard_catalog(p_table)
|
|
131
|
+
where kind = 'text' and column_name ~ '(user|author|by$|owner|who|person)' and not (column_name = any (v_skip))
|
|
132
|
+
order by ordinal limit 1), v_fk),
|
|
133
|
+
'body', v_text);
|
|
134
|
+
when 'media_list' then
|
|
135
|
+
v_out := v_out || jsonb_build_object('form_page', null, 'title', coalesce(v_display, v_pk), 'description', coalesce(v_fk, v_text), 'badge', null);
|
|
136
|
+
when 'content_row' then
|
|
137
|
+
v_out := v_out || jsonb_build_object('form_page', null, 'title', coalesce(v_display, v_pk), 'description', coalesce(v_text, v_fk),
|
|
138
|
+
'misc', coalesce(v_date, case when v_text is not null then v_fk end), 'badge', null);
|
|
139
|
+
when 'comments' then
|
|
140
|
+
v_out := v_out || jsonb_build_object(
|
|
141
|
+
'user', coalesce((select column_name from meta.wizard_catalog(p_table)
|
|
142
|
+
where (kind = 'text' or fk_table is not null) and column_name ~ '(user|author|by$|owner|who|person|name)'
|
|
143
|
+
and not is_pk and not (column_name = any (v_skip)) order by (fk_table is not null) desc, ordinal limit 1), v_fk, v_display),
|
|
144
|
+
'date', v_date, 'comment', coalesce(v_text, v_display));
|
|
145
|
+
when 'data_load' then
|
|
146
|
+
v_out := v_out || jsonb_build_object('mode', case when v_pk is not null then 'merge' else 'append' end, 'skip_errors', false,
|
|
147
|
+
'definition', left(upper(regexp_replace((select relname from pg_class where oid = p_table), '[^A-Za-z0-9_]', '_', 'g')), 55) || '_LOAD');
|
|
148
|
+
when 'wizard' then
|
|
149
|
+
select count(*) into v_n from meta.wizard_catalog(p_table)
|
|
150
|
+
where not generated and not is_pk and kind not in ('binary', 'geometry') and not (column_name = any (v_skip));
|
|
151
|
+
v_out := v_out || jsonb_build_object('return_page', null,
|
|
152
|
+
'columns', (select coalesce(jsonb_agg(column_name order by ordinal), '[]') from meta.wizard_catalog(p_table)
|
|
153
|
+
where not generated and not is_pk and kind not in ('binary', 'geometry') and not (column_name = any (v_skip))),
|
|
154
|
+
'steps', least(greatest(ceil(v_n / 4.0)::int, 2), 5));
|
|
155
|
+
end case;
|
|
156
|
+
return v_out;
|
|
157
|
+
end
|
|
158
|
+
$$;
|
|
159
|
+
|
|
160
|
+
-- the steps of a wizard page type: a page per step with a wizard progress list, the step's fields,
|
|
161
|
+
-- Cancel / Previous / Next, and on the last one Finish with an INSERT of all steps' values
|
|
162
|
+
create function meta.wizard_steps(p_app meta.app, p_table regclass, p_page int, p_label text, o jsonb) returns int
|
|
163
|
+
language plpgsql as $$
|
|
164
|
+
declare
|
|
165
|
+
v_skip text[] := meta.wizard_sidecars(p_table);
|
|
166
|
+
v_return int := coalesce(nullif(o ->> 'return_page', '')::int, p_app.home_page, p_page);
|
|
167
|
+
v_cols text[];
|
|
168
|
+
v_steps int;
|
|
169
|
+
v_list text := 'WIZARD_' || p_page;
|
|
170
|
+
v_first int;
|
|
171
|
+
v_page int;
|
|
172
|
+
v_region int;
|
|
173
|
+
v_seq int;
|
|
174
|
+
v_type text;
|
|
175
|
+
v_lov text;
|
|
176
|
+
v_names text := '';
|
|
177
|
+
v_values text := '';
|
|
178
|
+
i int;
|
|
179
|
+
c record;
|
|
180
|
+
begin
|
|
181
|
+
-- the chosen columns, and every column a row can't be saved without
|
|
182
|
+
select array_agg(w.column_name order by w.ordinal) into v_cols
|
|
183
|
+
from meta.wizard_catalog(p_table) w
|
|
184
|
+
where not w.generated and w.kind not in ('binary', 'geometry') and not (w.column_name = any (v_skip))
|
|
185
|
+
and (w.column_name in (select meta.wizard_col(p_table, x, 'wizard column') from jsonb_array_elements_text(coalesce(o -> 'columns', '[]')) x)
|
|
186
|
+
or (w.not_null and not w.has_default));
|
|
187
|
+
if v_cols is null then
|
|
188
|
+
raise exception 'choose the columns of the wizard';
|
|
189
|
+
end if;
|
|
190
|
+
v_steps := least(greatest(coalesce(nullif(o ->> 'steps', '')::int, 2), 1), 10, cardinality(v_cols));
|
|
191
|
+
for i in 0 .. v_steps - 1 loop
|
|
192
|
+
if exists (select 1 from meta.page where app_id = p_app.id and page_no = p_page + i) then
|
|
193
|
+
raise exception 'page % already exists (the wizard needs pages % to %)', p_page + i, p_page, p_page + v_steps - 1;
|
|
194
|
+
end if;
|
|
195
|
+
end loop;
|
|
196
|
+
if exists (select 1 from meta.list where app_id = p_app.id and name = v_list) then
|
|
197
|
+
raise exception 'the list % exists already', v_list;
|
|
198
|
+
end if;
|
|
199
|
+
insert into meta.list (app_id, name, type, description) values (p_app.id, v_list, 'static', 'The steps of the wizard on pages ' || p_page || '–' || (p_page + v_steps - 1));
|
|
200
|
+
for i in 0 .. v_steps - 1 loop
|
|
201
|
+
insert into meta.list_entry (app_id, list_name, seq, label, target_page) values (p_app.id, v_list, (i + 1) * 10, 'Step ' || (i + 1), p_page + i);
|
|
202
|
+
end loop;
|
|
203
|
+
|
|
204
|
+
for i in 0 .. v_steps - 1 loop
|
|
205
|
+
v_page := meta.wizard_new_page(p_app, p_page + i, p_label || case when v_steps > 1 then format(' (%s/%s)', i + 1, v_steps) else '' end, p_app.home_page);
|
|
206
|
+
update meta.page set title = p_label where id = v_page;
|
|
207
|
+
if i = 0 then
|
|
208
|
+
v_first := v_page;
|
|
209
|
+
end if;
|
|
210
|
+
insert into meta.region (page_id, seq, title, type, template, config)
|
|
211
|
+
values (v_page, 10, 'Steps', 'list', 'plain', jsonb_build_object('list', v_list, 'template', 'wizard_progress'));
|
|
212
|
+
insert into meta.region (page_id, seq, title, type, template)
|
|
213
|
+
values (v_page, 20, 'Step ' || (i + 1), 'form', 'wizard') returning id into v_region;
|
|
214
|
+
v_seq := 10;
|
|
215
|
+
-- this step's share of the columns, in order
|
|
216
|
+
for c in
|
|
217
|
+
select w.* from unnest(v_cols) with ordinality u(col, n) join meta.wizard_catalog(p_table) w on w.column_name = u.col
|
|
218
|
+
where (u.n - 1) * v_steps / cardinality(v_cols) = i
|
|
219
|
+
order by u.n
|
|
220
|
+
loop
|
|
221
|
+
v_lov := null;
|
|
222
|
+
v_type := case
|
|
223
|
+
when c.fk_table is not null then 'select'
|
|
224
|
+
when c.kind = 'number' then 'number'
|
|
225
|
+
when c.kind = 'date' then 'date'
|
|
226
|
+
when c.kind = 'timestamp' then 'datetime'
|
|
227
|
+
when c.kind = 'boolean' then 'switch'
|
|
228
|
+
else 'text' end;
|
|
229
|
+
if v_type = 'select' then
|
|
230
|
+
v_lov := format('select %I as d, %I as r from %s order by 1', coalesce(c.fk_display, c.fk_column), c.fk_column, meta.wizard_qname(c.fk_table));
|
|
231
|
+
end if;
|
|
232
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, required)
|
|
233
|
+
values (v_page, v_region, v_seq, meta.wizard_item(p_page + i, c.column_name), meta.wizard_column_label(p_table, c.column_name),
|
|
234
|
+
v_type, v_lov, c.not_null and not c.has_default and v_type <> 'switch');
|
|
235
|
+
v_seq := v_seq + 10;
|
|
236
|
+
v_names := v_names || case when v_names = '' then '' else ', ' end || quote_ident(c.column_name);
|
|
237
|
+
v_values := v_values || case when v_values = '' then '' else ', ' end || format(':%s::%s', meta.wizard_item(p_page + i, c.column_name), c.type_sql);
|
|
238
|
+
end loop;
|
|
239
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot, slot)
|
|
240
|
+
values (v_page, v_region, 10, 'CANCEL', 'Cancel', 'redirect', v_return, false, 'close');
|
|
241
|
+
if i > 0 then
|
|
242
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot, slot)
|
|
243
|
+
values (v_page, v_region, 20, 'PREVIOUS', 'Previous', 'redirect', p_page + i - 1, false, 'previous');
|
|
244
|
+
end if;
|
|
245
|
+
if i < v_steps - 1 then
|
|
246
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot, slot)
|
|
247
|
+
values (v_page, v_region, 30, 'NEXT', 'Next', 'submit', p_page + i + 1, true, 'next');
|
|
248
|
+
end if;
|
|
249
|
+
end loop;
|
|
250
|
+
-- the last step: Finish saves the row (as the application's role)
|
|
251
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot, slot)
|
|
252
|
+
values (v_page, v_region, 30, 'FINISH', 'Finish', 'submit', v_return, true, 'next');
|
|
253
|
+
insert into meta.process (page_id, seq, name, type, code, when_button, success_message)
|
|
254
|
+
values (v_page, 10, 'Create ' || meta.wizard_table_label(p_table, false), 'sql',
|
|
255
|
+
format('insert into %s (%s)%svalues (%s)', meta.wizard_qname(p_table), v_names, E'\n', v_values), 'FINISH',
|
|
256
|
+
meta.wizard_table_label(p_table, false) || ' created.');
|
|
257
|
+
return v_first;
|
|
258
|
+
end
|
|
259
|
+
$$;
|
|
260
|
+
|
|
261
|
+
create function meta.generate_page(
|
|
262
|
+
p_app text,
|
|
263
|
+
p_kind text,
|
|
264
|
+
p_table regclass,
|
|
265
|
+
p_page int,
|
|
266
|
+
p_options jsonb default '{}'
|
|
267
|
+
) returns int
|
|
268
|
+
language plpgsql as $$
|
|
269
|
+
declare
|
|
270
|
+
v_app meta.app;
|
|
271
|
+
o jsonb;
|
|
272
|
+
v_label text;
|
|
273
|
+
v_flabel text;
|
|
274
|
+
v_icon text;
|
|
275
|
+
v_pk text;
|
|
276
|
+
v_page int;
|
|
277
|
+
v_region int;
|
|
278
|
+
v_report int;
|
|
279
|
+
v_fpage int;
|
|
280
|
+
v_link jsonb;
|
|
281
|
+
v_rep jsonb;
|
|
282
|
+
v_src text;
|
|
283
|
+
v_cfg jsonb;
|
|
284
|
+
v_a text;
|
|
285
|
+
v_b text;
|
|
286
|
+
v_c text;
|
|
287
|
+
v_d text;
|
|
288
|
+
v_name text;
|
|
289
|
+
v_facets jsonb := '[]';
|
|
290
|
+
v_names text[];
|
|
291
|
+
v_n int;
|
|
292
|
+
c record;
|
|
293
|
+
begin
|
|
294
|
+
if p_kind in ('report_form', 'grid', 'form', 'cards', 'calendar', 'chart', 'map', 'facets', 'master_detail') then
|
|
295
|
+
return meta.generate_page_100(p_app, p_kind, p_table, p_page, p_options);
|
|
296
|
+
end if;
|
|
297
|
+
select * into v_app from meta.app where alias = p_app;
|
|
298
|
+
if v_app.id is null then
|
|
299
|
+
raise exception 'application "%" does not exist', p_app;
|
|
300
|
+
end if;
|
|
301
|
+
if p_kind = 'dashboard' then
|
|
302
|
+
raise exception 'a dashboard is made by the builder (Create page → Dashboard): it reads the data to choose its charts';
|
|
303
|
+
end if;
|
|
304
|
+
if p_kind not in ('report', 'smart_filters', 'tree', 'timeline', 'media_list', 'content_row', 'comments', 'data_load', 'wizard',
|
|
305
|
+
'search', 'tasks', 'workflows') then
|
|
306
|
+
raise exception 'unknown page type "%"', p_kind;
|
|
307
|
+
end if;
|
|
308
|
+
if p_kind in ('tasks', 'workflows') then
|
|
309
|
+
p_table := null;
|
|
310
|
+
elsif p_kind <> 'search' or p_table is not null then
|
|
311
|
+
perform meta.wizard_check_table(p_table);
|
|
312
|
+
end if;
|
|
313
|
+
if p_options is null or jsonb_typeof(p_options) <> 'object' then
|
|
314
|
+
p_options := '{}';
|
|
315
|
+
end if;
|
|
316
|
+
-- an option given as JSON null or '' means "none"; a missing one takes the default
|
|
317
|
+
o := meta.wizard_defaults(p_kind, case when p_kind = 'search' then null else p_table end) || p_options;
|
|
318
|
+
v_pk := case when p_table is not null then meta.wizard_pk(p_table) end;
|
|
319
|
+
v_label := coalesce(nullif(btrim(o ->> 'label'), ''),
|
|
320
|
+
case when p_table is not null then meta.wizard_table_label(p_table) else initcap(replace(p_kind, '_', ' ')) end);
|
|
321
|
+
v_flabel := case when p_table is not null then meta.wizard_table_label(p_table, false) end;
|
|
322
|
+
v_icon := nullif(o ->> 'icon', '');
|
|
323
|
+
v_fpage := case when p_kind in ('report', 'smart_filters', 'tree', 'timeline', 'media_list', 'content_row', 'search')
|
|
324
|
+
then nullif(o ->> 'form_page', '')::int end;
|
|
325
|
+
if v_fpage = p_page then
|
|
326
|
+
raise exception 'the form page needs a page number of its own';
|
|
327
|
+
end if;
|
|
328
|
+
if v_fpage is not null and (p_table is null or v_pk is null) then
|
|
329
|
+
raise exception 'a form page needs a table with a single-column primary key%', coalesce(format(' (%s has none)', p_table), '');
|
|
330
|
+
end if;
|
|
331
|
+
if v_fpage is not null then
|
|
332
|
+
v_link := jsonb_build_object('page', v_fpage, 'items', jsonb_build_object(meta.wizard_item(v_fpage, v_pk), '#' || v_pk || '#'));
|
|
333
|
+
end if;
|
|
334
|
+
|
|
335
|
+
if p_kind = 'wizard' then
|
|
336
|
+
v_page := meta.wizard_steps(v_app, p_table, p_page, v_label, o);
|
|
337
|
+
|
|
338
|
+
elsif p_kind = 'data_load' then
|
|
339
|
+
v_name := upper(btrim(coalesce(o ->> 'definition', '')));
|
|
340
|
+
if v_name !~ '^[A-Z][A-Z0-9_]{0,59}$' then
|
|
341
|
+
raise exception 'the data load definition''s name is upper case letters, digits and _ (at most 60), e.g. ORDERS_LOAD';
|
|
342
|
+
end if;
|
|
343
|
+
if coalesce(o ->> 'mode', 'append') not in ('append', 'merge', 'replace') then
|
|
344
|
+
raise exception 'the mode is append, merge or replace';
|
|
345
|
+
end if;
|
|
346
|
+
if o ->> 'mode' = 'merge' and v_pk is null then
|
|
347
|
+
raise exception 'merge needs a table with a single-column primary key (% has none)', p_table;
|
|
348
|
+
end if;
|
|
349
|
+
if meta.wizard_qname(p_table) !~ '^[A-Za-z_][A-Za-z0-9_$]*([.][A-Za-z_][A-Za-z0-9_$]*)?$' then
|
|
350
|
+
raise exception 'a data load definition needs a table name without quotes (% has one)', p_table;
|
|
351
|
+
end if;
|
|
352
|
+
-- an existing definition of that name is used as it is
|
|
353
|
+
if not exists (select 1 from meta.data_load_def where app_id = v_app.id and name = v_name) then
|
|
354
|
+
insert into meta.data_load_def (app_id, name, description, table_name, mode, skip_errors)
|
|
355
|
+
values (v_app.id, v_name, 'Made by Create page → Data loading', meta.wizard_qname(p_table),
|
|
356
|
+
coalesce(o ->> 'mode', 'append'), coalesce((o ->> 'skip_errors')::boolean, false));
|
|
357
|
+
end if;
|
|
358
|
+
v_page := meta.wizard_new_page(v_app, p_page, v_label, v_app.home_page);
|
|
359
|
+
insert into meta.region (page_id, seq, title, type)
|
|
360
|
+
values (v_page, 10, v_label, 'form') returning id into v_region;
|
|
361
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, required, help)
|
|
362
|
+
values (v_page, v_region, 10, format('P%s_FILE', p_page), 'File', 'file', true,
|
|
363
|
+
'A CSV, Excel, JSON or XML file whose column names match the table''s columns.');
|
|
364
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot)
|
|
365
|
+
values (v_page, v_region, 10, 'LOAD', 'Load', 'submit', true);
|
|
366
|
+
insert into meta.process (page_id, seq, name, type, when_button, success_message, config)
|
|
367
|
+
values (v_page, 10, 'Load ' || meta.wizard_table_label(p_table), 'data_load', 'LOAD', '{inserted} added, {updated} updated.',
|
|
368
|
+
jsonb_build_object('file_item', format('P%s_FILE', p_page), 'definition', v_name));
|
|
369
|
+
|
|
370
|
+
else
|
|
371
|
+
v_page := meta.wizard_new_page(v_app, p_page, v_label, v_app.home_page);
|
|
372
|
+
if p_kind in ('report', 'smart_filters') then
|
|
373
|
+
v_rep := meta.wizard_report(p_table);
|
|
374
|
+
insert into meta.region (page_id, seq, title, type, source, config)
|
|
375
|
+
values (v_page, 20, v_label, 'report', v_rep ->> 'source',
|
|
376
|
+
jsonb_build_object('headings', v_rep -> 'headings')
|
|
377
|
+
|| case when v_link is not null then jsonb_build_object('link', v_link || jsonb_build_object('column', v_pk, 'icon', true)) else '{}' end
|
|
378
|
+
|| case when jsonb_array_length(v_rep -> 'hidden') > 0 then jsonb_build_object('hidden', v_rep -> 'hidden') else '{}' end
|
|
379
|
+
|| case when v_rep -> 'formats' <> '{}' then jsonb_build_object('formats', v_rep -> 'formats') else '{}' end)
|
|
380
|
+
returning id into v_region;
|
|
381
|
+
if p_kind = 'smart_filters' then
|
|
382
|
+
for c in
|
|
383
|
+
select w.* from jsonb_array_elements_text(coalesce(o -> 'facets', '[]')) with ordinality x(col, n)
|
|
384
|
+
join meta.wizard_catalog(p_table) w on w.column_name = meta.wizard_col(p_table, x.col, 'facet column')
|
|
385
|
+
order by x.n
|
|
386
|
+
loop
|
|
387
|
+
continue when c.kind in ('binary', 'geometry', 'point', 'other');
|
|
388
|
+
-- a foreign key filters by the parent's name, the column wizard_report shows instead of the key
|
|
389
|
+
v_facets := v_facets || jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
|
|
390
|
+
'column', case when (v_rep -> 'hidden') ? c.column_name
|
|
391
|
+
then (select k from jsonb_object_keys(v_rep -> 'headings') k where k like c.column_name || '\_display%' order by length(k) limit 1)
|
|
392
|
+
else c.column_name end,
|
|
393
|
+
'label', meta.wizard_column_label(p_table, c.column_name),
|
|
394
|
+
'type', case when c.kind in ('number', 'date', 'timestamp') and c.fk_table is null then 'range' end)));
|
|
395
|
+
end loop;
|
|
396
|
+
insert into meta.region (page_id, seq, title, type, columns, template, config)
|
|
397
|
+
values (v_page, 10, 'Filters', 'smart_filters', 12, 'plain',
|
|
398
|
+
jsonb_build_object('report', v_region, 'facets', v_facets,
|
|
399
|
+
'placeholder', 'Search or filter ' || lower(meta.wizard_table_label(p_table)) || '…'));
|
|
400
|
+
end if;
|
|
401
|
+
|
|
402
|
+
elsif p_kind = 'tree' then
|
|
403
|
+
v_a := meta.wizard_col(p_table, o ->> 'id_column', 'id column');
|
|
404
|
+
v_b := meta.wizard_col(p_table, o ->> 'parent_column', 'parent column');
|
|
405
|
+
v_c := meta.wizard_col(p_table, o ->> 'label_column', 'label column');
|
|
406
|
+
if v_a is null then
|
|
407
|
+
raise exception 'choose the column that identifies a node (usually the primary key)';
|
|
408
|
+
end if;
|
|
409
|
+
if v_b is null then
|
|
410
|
+
raise exception 'choose the parent column: % has no foreign key to itself', p_table;
|
|
411
|
+
end if;
|
|
412
|
+
v_c := coalesce(v_c, v_a);
|
|
413
|
+
v_cfg := jsonb_build_object('expanded', least(greatest(coalesce(nullif(o ->> 'expanded', '')::int, 1), 0), 20));
|
|
414
|
+
if v_link is not null then
|
|
415
|
+
-- the tree's link takes #id#: the node's id column is the key here
|
|
416
|
+
v_cfg := v_cfg || jsonb_build_object('link', jsonb_build_object('page', v_fpage,
|
|
417
|
+
'items', jsonb_build_object(meta.wizard_item(v_fpage, v_pk), case when v_a = v_pk then '#id#' else '#' || v_pk || '#' end)));
|
|
418
|
+
end if;
|
|
419
|
+
insert into meta.region (page_id, seq, title, type, source, config)
|
|
420
|
+
values (v_page, 10, v_label, 'tree',
|
|
421
|
+
format(E'select t.%I as id,\n t.%I as parent_id,\n %s as label%s\n from %s t\n order by 3',
|
|
422
|
+
v_a, v_b, meta.wizard_expr(p_table, v_c),
|
|
423
|
+
case when v_link is not null and v_a <> v_pk then format(E',\n t.%I', v_pk) else '' end,
|
|
424
|
+
meta.wizard_qname(p_table)),
|
|
425
|
+
v_cfg)
|
|
426
|
+
returning id into v_region;
|
|
427
|
+
|
|
428
|
+
elsif p_kind in ('timeline', 'media_list', 'content_row', 'comments') then
|
|
429
|
+
-- a built-in template component, one row per instance, all rows in its wrapper ("multiple")
|
|
430
|
+
if p_kind = 'timeline' then
|
|
431
|
+
v_a := meta.wizard_col(p_table, o ->> 'title', 'title column');
|
|
432
|
+
v_b := meta.wizard_col(p_table, o ->> 'when', 'date column');
|
|
433
|
+
if v_a is null then
|
|
434
|
+
raise exception 'choose the column for the titles';
|
|
435
|
+
end if;
|
|
436
|
+
v_src := concat_ws(E',\n ',
|
|
437
|
+
meta.wizard_expr(p_table, v_a) || ' as title',
|
|
438
|
+
case when v_b is not null then format('t.%I as "when"', v_b) end,
|
|
439
|
+
meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'who', 'who column')) || ' as who',
|
|
440
|
+
meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'body', 'text column')) || ' as body',
|
|
441
|
+
case when v_pk is not null then format('t.%I', v_pk) end);
|
|
442
|
+
v_d := case when v_b is not null then format('t.%I desc nulls last', v_b) else '1' end;
|
|
443
|
+
elsif p_kind in ('media_list', 'content_row') then
|
|
444
|
+
v_a := meta.wizard_col(p_table, o ->> 'title', 'title column');
|
|
445
|
+
if v_a is null then
|
|
446
|
+
raise exception 'choose the column for the titles';
|
|
447
|
+
end if;
|
|
448
|
+
v_src := concat_ws(E',\n ',
|
|
449
|
+
meta.wizard_expr(p_table, v_a) || ' as title',
|
|
450
|
+
format('upper(left(%s::text, 2)) as initials', meta.wizard_expr(p_table, v_a)),
|
|
451
|
+
meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'description', 'description column')) || ' as description',
|
|
452
|
+
case when p_kind = 'content_row' then meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'misc', 'secondary column')) || ' as misc' end,
|
|
453
|
+
meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'badge', 'badge column')) || ' as badge',
|
|
454
|
+
-- the badge's colour from its value (approved, pending, late…)
|
|
455
|
+
meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'badge', 'badge column')) || ' as state',
|
|
456
|
+
case when v_pk is not null then format('t.%I', v_pk) end);
|
|
457
|
+
v_d := '1';
|
|
458
|
+
else
|
|
459
|
+
v_a := meta.wizard_col(p_table, o ->> 'comment', 'comment column');
|
|
460
|
+
v_b := meta.wizard_col(p_table, o ->> 'date', 'date column');
|
|
461
|
+
v_c := meta.wizard_col(p_table, o ->> 'user', 'user column');
|
|
462
|
+
if v_a is null then
|
|
463
|
+
raise exception 'choose the column with the comment';
|
|
464
|
+
end if;
|
|
465
|
+
v_src := concat_ws(E',\n ',
|
|
466
|
+
coalesce(meta.wizard_expr(p_table, v_c), quote_literal('')) || ' as "user"',
|
|
467
|
+
case when v_c is not null then format('upper(left(%s::text, 2)) as initials', meta.wizard_expr(p_table, v_c)) end,
|
|
468
|
+
case when v_b is not null then format('t.%I as date', v_b) end,
|
|
469
|
+
meta.wizard_expr(p_table, v_a) || ' as comment',
|
|
470
|
+
case when v_pk is not null then format('t.%I', v_pk) end);
|
|
471
|
+
v_d := case when v_b is not null then format('t.%I desc nulls last', v_b) else '1' end;
|
|
472
|
+
end if;
|
|
473
|
+
insert into meta.region (page_id, seq, title, type, source, config)
|
|
474
|
+
values (v_page, 10, v_label, 'template_component',
|
|
475
|
+
format(E'select %s\n from %s t\n order by %s', v_src, meta.wizard_qname(p_table), v_d),
|
|
476
|
+
jsonb_strip_nulls(jsonb_build_object('component', 'ut_' || p_kind, 'display', 'multiple', 'max_rows', 100, 'link', v_link)))
|
|
477
|
+
returning id into v_region;
|
|
478
|
+
|
|
479
|
+
elsif p_kind = 'search' then
|
|
480
|
+
select array_agg(upper(x)) into v_names from jsonb_array_elements_text(coalesce(o -> 'configs', '[]')) x where btrim(x) <> '';
|
|
481
|
+
for v_name in select unnest(coalesce(v_names, '{}')) loop
|
|
482
|
+
if not exists (select 1 from meta.search_config where app_id = v_app.id and name = v_name) then
|
|
483
|
+
raise exception 'the application has no search configuration %', v_name;
|
|
484
|
+
end if;
|
|
485
|
+
end loop;
|
|
486
|
+
if p_table is not null then
|
|
487
|
+
-- a new configuration on the table: its text columns searched, its name or first text column as the title
|
|
488
|
+
v_name := left(upper(regexp_replace((select relname from pg_class where oid = p_table), '[^A-Za-z0-9_]', '_', 'g')), 50);
|
|
489
|
+
v_n := 1;
|
|
490
|
+
while exists (select 1 from meta.search_config where app_id = v_app.id and name = v_name || case when v_n > 1 then '_' || v_n else '' end) loop
|
|
491
|
+
v_n := v_n + 1;
|
|
492
|
+
end loop;
|
|
493
|
+
v_name := v_name || case when v_n > 1 then '_' || v_n else '' end;
|
|
494
|
+
v_a := coalesce(meta.wizard_display(p_table), v_pk);
|
|
495
|
+
if v_a is null then
|
|
496
|
+
raise exception '% has no text column to show as the results'' titles', p_table;
|
|
497
|
+
end if;
|
|
498
|
+
insert into meta.search_config (app_id, name, label, seq, source_type, source, searchable_columns, title_column, description_column, link)
|
|
499
|
+
values (v_app.id, v_name, meta.wizard_table_label(p_table), 10 * (1 + (select count(*) from meta.search_config where app_id = v_app.id)),
|
|
500
|
+
'table', meta.wizard_qname(p_table),
|
|
501
|
+
coalesce((select array_agg(column_name order by ordinal) from meta.wizard_catalog(p_table)
|
|
502
|
+
where kind = 'text' and not (column_name = any (meta.wizard_sidecars(p_table)))), '{}'),
|
|
503
|
+
v_a,
|
|
504
|
+
(select column_name from meta.wizard_catalog(p_table) where kind = 'text' and column_name <> v_a
|
|
505
|
+
and not (column_name = any (meta.wizard_sidecars(p_table))) order by ordinal limit 1),
|
|
506
|
+
v_link);
|
|
507
|
+
v_names := coalesce(v_names, '{}') || v_name;
|
|
508
|
+
end if;
|
|
509
|
+
if coalesce(cardinality(v_names), 0) = 0 then
|
|
510
|
+
raise exception 'choose at least one search configuration, or a table to search';
|
|
511
|
+
end if;
|
|
512
|
+
insert into meta.region (page_id, seq, title, type, columns, config)
|
|
513
|
+
values (v_page, 10, v_label, 'search', 12, jsonb_build_object('configs', to_jsonb(v_names)))
|
|
514
|
+
returning id into v_region;
|
|
515
|
+
-- the form page belongs to the new configuration's table
|
|
516
|
+
if p_table is null then
|
|
517
|
+
v_fpage := null;
|
|
518
|
+
end if;
|
|
519
|
+
|
|
520
|
+
elsif p_kind = 'tasks' then
|
|
521
|
+
insert into meta.region (page_id, seq, title, type, columns, config)
|
|
522
|
+
values (v_page, 10, v_label, 'tasks', 12,
|
|
523
|
+
jsonb_build_object('context', case when o ->> 'context' in ('my', 'initiated', 'admin') then o ->> 'context' else 'my' end,
|
|
524
|
+
'completed', coalesce((o ->> 'completed')::boolean, false)));
|
|
525
|
+
elsif p_kind = 'workflows' then
|
|
526
|
+
insert into meta.region (page_id, seq, title, type, columns, config)
|
|
527
|
+
values (v_page, 10, v_label, 'workflows', 12,
|
|
528
|
+
case when o ->> 'context' = 'admin' then '{"context": "admin"}'::jsonb else '{}'::jsonb end);
|
|
529
|
+
end if;
|
|
530
|
+
|
|
531
|
+
if v_fpage is not null then
|
|
532
|
+
perform meta.wizard_form(v_app, p_table, v_fpage, v_flabel, p_page, true, null);
|
|
533
|
+
if p_kind <> 'search' then
|
|
534
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot)
|
|
535
|
+
values (v_page, v_region, 10, 'CREATE', 'Create', 'redirect', v_fpage, true);
|
|
536
|
+
end if;
|
|
537
|
+
end if;
|
|
538
|
+
end if;
|
|
539
|
+
|
|
540
|
+
if coalesce((o ->> 'nav')::boolean, false) then
|
|
541
|
+
perform meta.wizard_nav(v_app, v_label, v_icon, p_page);
|
|
542
|
+
end if;
|
|
543
|
+
return v_page;
|
|
544
|
+
end
|
|
545
|
+
$$;
|
|
546
|
+
|
|
547
|
+
revoke all on function meta.generate_page_100(text, text, regclass, int, jsonb), meta.wizard_defaults_097(text, regclass),
|
|
548
|
+
meta.generate_page(text, text, regclass, int, jsonb), meta.wizard_defaults(text, regclass),
|
|
549
|
+
meta.wizard_check_table(regclass), meta.wizard_steps(meta.app, regclass, int, text, jsonb)
|
|
550
|
+
from public;
|
|
551
|
+
|
|
552
|
+
-- ---------------------------------------------------------------- 2. configuration options
|
|
553
|
+
alter table meta.build_option add column configurable boolean not null default false;
|
|
554
|
+
comment on column meta.build_option.configurable is 'The application''s administrators may switch it at run time (the Configuration Options feature; meta.set_configurable_options)';
|
|
555
|
+
|
|
556
|
+
-- an export made before 137 leaves the flag out
|
|
557
|
+
create function meta.build_option_defaults() returns trigger
|
|
558
|
+
language plpgsql set search_path = pg_catalog as $$
|
|
559
|
+
begin
|
|
560
|
+
new.configurable := coalesce(new.configurable, false);
|
|
561
|
+
return new;
|
|
562
|
+
end $$;
|
|
563
|
+
create trigger build_option_defaults before insert on meta.build_option
|
|
564
|
+
for each row execute function meta.build_option_defaults();
|
|
565
|
+
|
|
566
|
+
-- the configurable build options of the current application, for its administrators
|
|
567
|
+
create function meta.configurable_options()
|
|
568
|
+
returns table (name text, description text, enabled boolean)
|
|
569
|
+
language plpgsql stable security definer set search_path = meta, pg_catalog as $$
|
|
570
|
+
declare
|
|
571
|
+
v_app int := meta.caller_app('meta.configurable_options');
|
|
572
|
+
begin
|
|
573
|
+
if not meta.has_role('admin') then
|
|
574
|
+
raise exception 'meta.configurable_options: for the application''s administrators' using errcode = '42501';
|
|
575
|
+
end if;
|
|
576
|
+
return query
|
|
577
|
+
select o.name, coalesce(nullif(btrim(o.description), ''), initcap(replace(o.name, '_', ' '))), o.status = 'include'
|
|
578
|
+
from meta.build_option o
|
|
579
|
+
where o.app_id = v_app and o.configurable
|
|
580
|
+
order by o.name;
|
|
581
|
+
end
|
|
582
|
+
$$;
|
|
583
|
+
grant execute on function meta.configurable_options() to public;
|
|
584
|
+
|
|
585
|
+
-- switch the configurable build options: those named in p_enabled (colon-separated, as a checkbox group
|
|
586
|
+
-- posts them) are included, the other configurable ones excluded. Returns how many changed.
|
|
587
|
+
create function meta.set_configurable_options(p_enabled text) returns int
|
|
588
|
+
language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
|
|
589
|
+
declare
|
|
590
|
+
v_app int := meta.caller_app('meta.set_configurable_options');
|
|
591
|
+
v_on text[] := coalesce(string_to_array(nullif(p_enabled, ''), ':'), '{}');
|
|
592
|
+
v_n int;
|
|
593
|
+
begin
|
|
594
|
+
if not meta.has_role('admin') then
|
|
595
|
+
raise exception 'meta.set_configurable_options: for the application''s administrators' using errcode = '42501';
|
|
596
|
+
end if;
|
|
597
|
+
if exists (select 1 from unnest(v_on) x
|
|
598
|
+
where not exists (select 1 from meta.build_option o where o.app_id = v_app and o.configurable and o.name = upper(x))) then
|
|
599
|
+
raise exception 'meta.set_configurable_options: only configurable build options can be switched' using errcode = '42501';
|
|
600
|
+
end if;
|
|
601
|
+
update meta.build_option o
|
|
602
|
+
set status = case when o.name = any (select upper(x) from unnest(v_on) x) then 'include' else 'exclude' end
|
|
603
|
+
where o.app_id = v_app and o.configurable
|
|
604
|
+
and o.status <> case when o.name = any (select upper(x) from unnest(v_on) x) then 'include' else 'exclude' end;
|
|
605
|
+
get diagnostics v_n = row_count;
|
|
606
|
+
return v_n;
|
|
607
|
+
end
|
|
608
|
+
$$;
|
|
609
|
+
grant execute on function meta.set_configurable_options(text) to public;
|
|
610
|
+
|
|
611
|
+
-- ---------------------------------------------------------------- 3. e-mail reporting
|
|
612
|
+
create function meta.send_activity_report(p_to text, p_days int default 7) returns bigint
|
|
613
|
+
language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
|
|
614
|
+
declare
|
|
615
|
+
v_app int := meta.caller_app('meta.send_activity_report');
|
|
616
|
+
v_auto int := nullif(current_setting('pgkiln.automation_id', true), '')::int;
|
|
617
|
+
v_session uuid := nullif(current_setting('pgkiln.session_id', true), '')::uuid;
|
|
618
|
+
v_days int := least(greatest(coalesce(p_days, 7), 1), 366);
|
|
619
|
+
v_name text;
|
|
620
|
+
v_body text;
|
|
621
|
+
v_mail bigint;
|
|
622
|
+
r record;
|
|
623
|
+
begin
|
|
624
|
+
-- the application's own automations (the scheduler sets the automation), or a signed-in administrator
|
|
625
|
+
if not ((v_auto is not null and exists (select 1 from meta.automation a where a.id = v_auto and a.app_id = v_app))
|
|
626
|
+
or (v_session is not null and exists (select 1 from meta.session s where s.id = v_session and s.app_id = v_app)
|
|
627
|
+
and meta.has_role('admin'))) then
|
|
628
|
+
raise exception 'meta.send_activity_report: for the application''s automations and administrators' using errcode = '42501';
|
|
629
|
+
end if;
|
|
630
|
+
if coalesce(btrim(p_to), '') = '' then
|
|
631
|
+
return null;
|
|
632
|
+
end if;
|
|
633
|
+
select a.name into v_name from meta.app a where a.id = v_app;
|
|
634
|
+
select count(*) filter (where l.event = 'page_view') as views,
|
|
635
|
+
count(distinct l.username) filter (where l.event = 'page_view') as users,
|
|
636
|
+
count(*) filter (where l.event = 'login') as logins,
|
|
637
|
+
count(*) filter (where l.event = 'login_failed') as failed,
|
|
638
|
+
count(*) filter (where l.event ~ 'error') as errors
|
|
639
|
+
into r
|
|
640
|
+
from meta.activity_log l
|
|
641
|
+
where l.app_id = v_app and l.at > now() - make_interval(days => v_days);
|
|
642
|
+
v_body := format(E'%s: the last %s days\n\nPage views: %s\nUsers: %s\nSign-ins: %s (failed: %s)\nErrors: %s\n',
|
|
643
|
+
v_name, v_days, r.views, r.users, r.logins, r.failed, r.errors);
|
|
644
|
+
v_body := v_body || E'\nMost viewed pages:\n' || coalesce((
|
|
645
|
+
select string_agg(format(' page %s: %s', x.page_no, x.n), E'\n' order by x.n desc, x.page_no)
|
|
646
|
+
from (select l.page_no, count(*) as n from meta.activity_log l
|
|
647
|
+
where l.app_id = v_app and l.event = 'page_view' and l.at > now() - make_interval(days => v_days)
|
|
648
|
+
group by 1 order by 2 desc, 1 limit 5) x), ' (none)')
|
|
649
|
+
|| E'\n\nThe activity is attached as a CSV file.\n';
|
|
650
|
+
v_mail := meta.send_mail(p_to, format('%s: activity of the last %s days', v_name, v_days), v_body);
|
|
651
|
+
perform meta.add_mail_attachment(v_mail,
|
|
652
|
+
meta.data_export(format('select at, username, event, page_no, elapsed_ms from meta.activity_log where app_id = %s and at > now() - make_interval(days => %s) order by at desc limit 10000', v_app, v_days), 'csv'),
|
|
653
|
+
'activity.csv', 'text/csv');
|
|
654
|
+
return v_mail;
|
|
655
|
+
end
|
|
656
|
+
$$;
|
|
657
|
+
revoke all on function meta.send_activity_report(text, int) from public;
|
|
658
|
+
grant execute on function meta.send_activity_report(text, int) to public;
|
|
659
|
+
comment on function meta.send_activity_report(text, int) is 'Mail a summary of the current application''s activity with a CSV (137; the Email Reporting feature). For its automations and administrators.';
|
|
660
|
+
|
|
661
|
+
-- ---------------------------------------------------------------- 4. copy page
|
|
662
|
+
-- does the application have a shared component of this kind and name?
|
|
663
|
+
create function meta.shared_component_exists(p_kind text, p_app int, p_name text) returns boolean
|
|
664
|
+
language sql stable as $$
|
|
665
|
+
select case p_kind
|
|
666
|
+
when 'authz_scheme' then exists (select 1 from meta.authz_scheme s where s.app_id = p_app and s.name = p_name)
|
|
667
|
+
when 'build_option' then exists (select 1 from meta.build_option s where s.app_id = p_app and s.name = p_name)
|
|
668
|
+
when 'lov' then exists (select 1 from meta.lov s where s.app_id = p_app and s.name = p_name)
|
|
669
|
+
when 'list' then exists (select 1 from meta.list s where s.app_id = p_app and s.name = p_name)
|
|
670
|
+
when 'template_component' then exists (select 1 from meta.template_component s where s.app_id = p_app and s.static_id = p_name)
|
|
671
|
+
when 'search_config' then exists (select 1 from meta.search_config s where s.app_id = p_app and s.name = p_name)
|
|
672
|
+
when 'data_load_def' then exists (select 1 from meta.data_load_def s where s.app_id = p_app and s.name = p_name)
|
|
673
|
+
else false end
|
|
674
|
+
$$;
|
|
675
|
+
|
|
676
|
+
-- the shared components a page refers to by name, that the source application has:
|
|
677
|
+
-- (kind, name, whether the target application has one of that name)
|
|
678
|
+
create function meta.page_shared_refs(p_page_id int, p_target_app int)
|
|
679
|
+
returns table (kind text, name text, in_target boolean)
|
|
680
|
+
language sql stable as $$
|
|
681
|
+
with pg as (select * from meta.page where id = p_page_id),
|
|
682
|
+
refs (kind, name) as (
|
|
683
|
+
select 'authz_scheme', x.authz from (
|
|
684
|
+
select authz from pg union all select authz from meta.region where page_id = p_page_id
|
|
685
|
+
union all select authz from meta.item where page_id = p_page_id union all select authz from meta.button where page_id = p_page_id
|
|
686
|
+
union all select authz from meta.process where page_id = p_page_id union all select authz from meta.dynamic_action where page_id = p_page_id
|
|
687
|
+
union all select authz from meta.branch where page_id = p_page_id union all select authz from meta.computation where page_id = p_page_id) x
|
|
688
|
+
union
|
|
689
|
+
select 'build_option', ltrim(x.build_option, '!') from (
|
|
690
|
+
select build_option from pg union all select build_option from meta.region where page_id = p_page_id
|
|
691
|
+
union all select build_option from meta.item where page_id = p_page_id union all select build_option from meta.button where page_id = p_page_id
|
|
692
|
+
union all select build_option from meta.process where page_id = p_page_id union all select build_option from meta.dynamic_action where page_id = p_page_id
|
|
693
|
+
union all select build_option from meta.validation where page_id = p_page_id union all select build_option from meta.branch where page_id = p_page_id
|
|
694
|
+
union all select build_option from meta.computation where page_id = p_page_id) x
|
|
695
|
+
union
|
|
696
|
+
select 'lov', substr(i.lov, 5) from meta.item i where i.page_id = p_page_id and i.lov ~ '^LOV:'
|
|
697
|
+
union
|
|
698
|
+
select 'list', r.config ->> 'list' from meta.region r where r.page_id = p_page_id and r.type = 'list'
|
|
699
|
+
union
|
|
700
|
+
select 'template_component', r.config ->> 'component' from meta.region r where r.page_id = p_page_id and r.type = 'template_component'
|
|
701
|
+
union
|
|
702
|
+
select 'search_config', upper(x) from meta.region r, jsonb_array_elements_text(case when jsonb_typeof(r.config -> 'configs') = 'array' then r.config -> 'configs' else '[]' end) x
|
|
703
|
+
where r.page_id = p_page_id and r.type = 'search'
|
|
704
|
+
union
|
|
705
|
+
select 'data_load_def', p.config ->> 'definition' from meta.process p where p.page_id = p_page_id and p.type = 'data_load'
|
|
706
|
+
)
|
|
707
|
+
select r.kind, r.name,
|
|
708
|
+
meta.shared_component_exists(r.kind, p_target_app, r.name)
|
|
709
|
+
from refs r, pg
|
|
710
|
+
-- a built-in template component (ut_…) or a name the application doesn't have is left as it is
|
|
711
|
+
where coalesce(r.name, '') <> '' and meta.shared_component_exists(r.kind, pg.app_id, r.name)
|
|
712
|
+
order by 1, 2
|
|
713
|
+
$$;
|
|
714
|
+
|
|
715
|
+
-- {"page": <old>} → {"page": <new>} anywhere in a configuration (a page's links to itself)
|
|
716
|
+
create function meta.copy_page_json(p jsonb, p_old int, p_new int) returns jsonb
|
|
717
|
+
language plpgsql immutable as $$
|
|
718
|
+
declare
|
|
719
|
+
v jsonb;
|
|
720
|
+
k text;
|
|
721
|
+
e jsonb;
|
|
722
|
+
begin
|
|
723
|
+
if p is null or p_old = p_new then
|
|
724
|
+
return p;
|
|
725
|
+
end if;
|
|
726
|
+
case jsonb_typeof(p)
|
|
727
|
+
when 'object' then
|
|
728
|
+
v := '{}';
|
|
729
|
+
for k, e in select * from jsonb_each(p) loop
|
|
730
|
+
if k = 'page' and jsonb_typeof(e) = 'number' and (e #>> '{}')::numeric = p_old then
|
|
731
|
+
v := v || jsonb_build_object(k, p_new);
|
|
732
|
+
else
|
|
733
|
+
v := v || jsonb_build_object(k, meta.copy_page_json(e, p_old, p_new));
|
|
734
|
+
end if;
|
|
735
|
+
end loop;
|
|
736
|
+
return v;
|
|
737
|
+
when 'array' then
|
|
738
|
+
return coalesce((select jsonb_agg(meta.copy_page_json(x, p_old, p_new) order by n) from jsonb_array_elements(p) with ordinality a(x, n)), '[]');
|
|
739
|
+
else
|
|
740
|
+
return p;
|
|
741
|
+
end case;
|
|
742
|
+
end
|
|
743
|
+
$$;
|
|
744
|
+
|
|
745
|
+
/*
|
|
746
|
+
* Copy page p_page of application p_from into application p_to (the same or another) as page p_new.
|
|
747
|
+
* p_map: {"<kind>:<NAME>": "<target name>"} maps a shared component to one of the target's (by name);
|
|
748
|
+
* without an entry, a component the target has by the same name is used, else it is copied.
|
|
749
|
+
* Returns the new page's id.
|
|
750
|
+
*/
|
|
751
|
+
create function meta.copy_page(p_from int, p_page int, p_to int, p_new int, p_name text default null, p_map jsonb default '{}')
|
|
752
|
+
returns int
|
|
753
|
+
language plpgsql as $$
|
|
754
|
+
declare
|
|
755
|
+
v_src meta.page;
|
|
756
|
+
v_page_id int;
|
|
757
|
+
v_old text;
|
|
758
|
+
v_new text;
|
|
759
|
+
v_rmap jsonb := '{}';
|
|
760
|
+
v_map jsonb := '{}';
|
|
761
|
+
v_target text;
|
|
762
|
+
v_id int;
|
|
763
|
+
v_newid int;
|
|
764
|
+
v_e jsonb;
|
|
765
|
+
v_lmap jsonb;
|
|
766
|
+
t text;
|
|
767
|
+
r record;
|
|
768
|
+
x record;
|
|
769
|
+
begin
|
|
770
|
+
select * into v_src from meta.page where app_id = p_from and page_no = p_page;
|
|
771
|
+
if v_src.id is null then
|
|
772
|
+
raise exception 'page % does not exist', p_page;
|
|
773
|
+
end if;
|
|
774
|
+
if not exists (select 1 from meta.app where id = p_to) then
|
|
775
|
+
raise exception 'the target application does not exist';
|
|
776
|
+
end if;
|
|
777
|
+
if p_new is null or p_new < 1 then
|
|
778
|
+
raise exception 'a page number must be a positive whole number';
|
|
779
|
+
end if;
|
|
780
|
+
if exists (select 1 from meta.page where app_id = p_to and page_no = p_new) then
|
|
781
|
+
raise exception 'page % already exists', p_new;
|
|
782
|
+
end if;
|
|
783
|
+
if p_map is null or jsonb_typeof(p_map) <> 'object' then
|
|
784
|
+
p_map := '{}';
|
|
785
|
+
end if;
|
|
786
|
+
|
|
787
|
+
-- the shared components: mapped, the same name, or copied
|
|
788
|
+
for r in select * from meta.page_shared_refs(v_src.id, p_to) loop
|
|
789
|
+
v_target := nullif(p_map ->> (r.kind || ':' || r.name), '');
|
|
790
|
+
if v_target is null and (p_from = p_to or r.in_target) then
|
|
791
|
+
v_target := r.name;
|
|
792
|
+
end if;
|
|
793
|
+
if v_target is not null then
|
|
794
|
+
if not meta.shared_component_exists(r.kind, p_to, v_target) then
|
|
795
|
+
raise exception 'the target application has no % %', replace(r.kind, '_', ' '), v_target;
|
|
796
|
+
end if;
|
|
797
|
+
v_map := v_map || jsonb_build_object(r.kind || ':' || r.name, v_target);
|
|
798
|
+
continue;
|
|
799
|
+
end if;
|
|
800
|
+
-- copy the component into the target application, under its own name
|
|
801
|
+
t := case r.kind when 'authz_scheme' then 'authz_scheme' when 'build_option' then 'build_option' when 'lov' then 'lov'
|
|
802
|
+
when 'list' then 'list' when 'template_component' then 'template_component'
|
|
803
|
+
when 'search_config' then 'search_config' when 'data_load_def' then 'data_load_def' end;
|
|
804
|
+
execute format(
|
|
805
|
+
'insert into meta.%1$I select (jsonb_populate_record(null::meta.%1$I, to_jsonb(x) || jsonb_build_object(''id'', nextval(''meta.%1$s_id_seq''), ''app_id'', $2))).* from meta.%1$I x where x.app_id = $1 and x.%2$I = $3',
|
|
806
|
+
t, case when t = 'template_component' then 'static_id' else 'name' end)
|
|
807
|
+
using p_from, p_to, r.name;
|
|
808
|
+
if r.kind = 'list' then
|
|
809
|
+
-- and its entries, with their parents
|
|
810
|
+
v_lmap := '{}';
|
|
811
|
+
for x in select * from meta.list_entry where app_id = p_from and list_name = r.name order by parent_id nulls first, id loop
|
|
812
|
+
v_e := to_jsonb(x) || jsonb_build_object('id', nextval('meta.list_entry_id_seq'), 'app_id', p_to,
|
|
813
|
+
'parent_id', v_lmap ->> x.parent_id::text);
|
|
814
|
+
insert into meta.list_entry select (jsonb_populate_record(null::meta.list_entry, v_e)).* returning id into v_id;
|
|
815
|
+
v_lmap := v_lmap || jsonb_build_object(x.id::text, v_id);
|
|
816
|
+
end loop;
|
|
817
|
+
end if;
|
|
818
|
+
v_map := v_map || jsonb_build_object(r.kind || ':' || r.name, r.name);
|
|
819
|
+
end loop;
|
|
820
|
+
|
|
821
|
+
-- the page
|
|
822
|
+
insert into meta.page
|
|
823
|
+
select (jsonb_populate_record(null::meta.page, to_jsonb(v_src) || jsonb_build_object(
|
|
824
|
+
'id', nextval('meta.page_id_seq'), 'app_id', p_to, 'page_no', p_new,
|
|
825
|
+
'name', coalesce(nullif(btrim(p_name), ''), v_src.name),
|
|
826
|
+
'parent_page', case when v_src.parent_page = p_page then null
|
|
827
|
+
when exists (select 1 from meta.page where app_id = p_to and page_no = v_src.parent_page) then v_src.parent_page
|
|
828
|
+
else (select home_page from meta.app where id = p_to) end,
|
|
829
|
+
'authz', v_map ->> ('authz_scheme:' || v_src.authz),
|
|
830
|
+
'build_option', case when v_src.build_option is not null
|
|
831
|
+
then case when left(v_src.build_option, 1) = '!' then '!' else '' end
|
|
832
|
+
|| (v_map ->> ('build_option:' || ltrim(v_src.build_option, '!'))) end))).*
|
|
833
|
+
returning id into v_page_id;
|
|
834
|
+
-- an authorization or build option that is not a shared component (e.g. a built-in) stays as it was
|
|
835
|
+
update meta.page p set authz = coalesce(p.authz, v_src.authz), build_option = coalesce(p.build_option, v_src.build_option) where p.id = v_page_id;
|
|
836
|
+
-- a page group the target application doesn't have is left out
|
|
837
|
+
update meta.page p set page_group = null
|
|
838
|
+
where p.id = v_page_id and p.page_group is not null and p_from <> p_to
|
|
839
|
+
and not exists (select 1 from meta.page_group g where g.app_id = p_to and g.name = p.page_group);
|
|
840
|
+
|
|
841
|
+
v_old := 'P' || p_page || '_';
|
|
842
|
+
v_new := 'P' || p_new || '_';
|
|
843
|
+
-- every component: items renamed P<old>_ → P<new>_ (also where code refers to them), links to the page itself
|
|
844
|
+
-- to the copy, shared components mapped
|
|
845
|
+
for t in select unnest(array['region', 'item', 'button', 'dynamic_action', 'validation', 'process', 'computation', 'branch']) loop
|
|
846
|
+
for v_e in execute format('select to_jsonb(x) from meta.%I x where x.page_id = $1 order by x.id', t) using v_src.id loop
|
|
847
|
+
v_id := (v_e ->> 'id')::int;
|
|
848
|
+
if p_page <> p_new then
|
|
849
|
+
v_e := regexp_replace(v_e::text, '\m' || v_old, v_new, 'g')::jsonb;
|
|
850
|
+
end if;
|
|
851
|
+
if v_e ? 'config' then
|
|
852
|
+
v_e := jsonb_set(v_e, '{config}', coalesce(meta.copy_page_json(v_e -> 'config', p_page, p_new), 'null'));
|
|
853
|
+
end if;
|
|
854
|
+
if v_e ? 'target_page' and (v_e ->> 'target_page')::int = p_page and coalesce(v_e ->> 'target_type', 'page') = 'page' then
|
|
855
|
+
v_e := v_e || jsonb_build_object('target_page', p_new);
|
|
856
|
+
end if;
|
|
857
|
+
if v_e ->> 'authz' is not null and v_map ? ('authz_scheme:' || (v_e ->> 'authz')) then
|
|
858
|
+
v_e := v_e || jsonb_build_object('authz', v_map ->> ('authz_scheme:' || (v_e ->> 'authz')));
|
|
859
|
+
end if;
|
|
860
|
+
if v_e ->> 'build_option' is not null and v_map ? ('build_option:' || ltrim(v_e ->> 'build_option', '!')) then
|
|
861
|
+
v_e := v_e || jsonb_build_object('build_option', case when left(v_e ->> 'build_option', 1) = '!' then '!' else '' end
|
|
862
|
+
|| (v_map ->> ('build_option:' || ltrim(v_e ->> 'build_option', '!'))));
|
|
863
|
+
end if;
|
|
864
|
+
if t = 'item' and v_e ->> 'lov' ~ '^LOV:' and v_map ? ('lov:' || substr(v_e ->> 'lov', 5)) then
|
|
865
|
+
v_e := v_e || jsonb_build_object('lov', 'LOV:' || (v_map ->> ('lov:' || substr(v_e ->> 'lov', 5))));
|
|
866
|
+
end if;
|
|
867
|
+
if t = 'region' and v_e ->> 'type' = 'list' and v_map ? ('list:' || (v_e #>> '{config,list}')) then
|
|
868
|
+
v_e := jsonb_set(v_e, '{config,list}', to_jsonb(v_map ->> ('list:' || (v_e #>> '{config,list}'))));
|
|
869
|
+
end if;
|
|
870
|
+
if t = 'region' and v_e ->> 'type' = 'template_component' and v_map ? ('template_component:' || (v_e #>> '{config,component}')) then
|
|
871
|
+
v_e := jsonb_set(v_e, '{config,component}', to_jsonb(v_map ->> ('template_component:' || (v_e #>> '{config,component}'))));
|
|
872
|
+
end if;
|
|
873
|
+
if t = 'region' and v_e ->> 'type' = 'search' and jsonb_typeof(v_e #> '{config,configs}') = 'array' then
|
|
874
|
+
v_e := jsonb_set(v_e, '{config,configs}', (select coalesce(jsonb_agg(coalesce(v_map ->> ('search_config:' || upper(c)), c) order by n), '[]')
|
|
875
|
+
from jsonb_array_elements_text(v_e #> '{config,configs}') with ordinality a(c, n)));
|
|
876
|
+
end if;
|
|
877
|
+
if t = 'process' and v_e ->> 'type' = 'data_load' and v_map ? ('data_load_def:' || (v_e #>> '{config,definition}')) then
|
|
878
|
+
v_e := jsonb_set(v_e, '{config,definition}', to_jsonb(v_map ->> ('data_load_def:' || (v_e #>> '{config,definition}'))));
|
|
879
|
+
end if;
|
|
880
|
+
v_e := v_e || jsonb_build_object('id', null, 'page_id', v_page_id);
|
|
881
|
+
if v_e ? 'region_id' then
|
|
882
|
+
v_e := v_e || jsonb_build_object('region_id', v_rmap -> (v_e ->> 'region_id'));
|
|
883
|
+
end if;
|
|
884
|
+
if v_e ? 'affected_region_id' then
|
|
885
|
+
v_e := v_e || jsonb_build_object('affected_region_id', v_rmap -> (v_e ->> 'affected_region_id'));
|
|
886
|
+
end if;
|
|
887
|
+
execute format('insert into meta.%1$I select (jsonb_populate_record(null::meta.%1$I, $1 || jsonb_build_object(''id'', nextval(''meta.%1$s_id_seq'')))).* returning id', t)
|
|
888
|
+
using v_e into v_newid;
|
|
889
|
+
if t = 'region' then
|
|
890
|
+
v_rmap := v_rmap || jsonb_build_object(v_id::text, v_newid);
|
|
891
|
+
end if;
|
|
892
|
+
end loop;
|
|
893
|
+
if t = 'region' then
|
|
894
|
+
-- facets, smart filters and maps point at their report region by id
|
|
895
|
+
update meta.region g
|
|
896
|
+
set config = jsonb_set(g.config, '{report}', v_rmap -> (g.config ->> 'report'))
|
|
897
|
+
where g.page_id = v_page_id and g.config ? 'report' and v_rmap ? (g.config ->> 'report');
|
|
898
|
+
end if;
|
|
899
|
+
end loop;
|
|
900
|
+
return v_page_id;
|
|
901
|
+
end
|
|
902
|
+
$$;
|
|
903
|
+
revoke all on function meta.shared_component_exists(text, int, text), meta.page_shared_refs(int, int), meta.copy_page_json(jsonb, int, int),
|
|
904
|
+
meta.copy_page(int, int, int, int, text, jsonb) from public;
|