pgkiln 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +11 -9
- package/db/migrations/087_login_page.sql +175 -0
- package/db/migrations/088_region_templates.sql +18 -0
- package/db/migrations/089_page_groups.sql +330 -0
- package/db/migrations/090_workspace_isolation.sql +512 -0
- package/docs/apex-feature-parity.md +23 -21
- package/docs/guide/03-builder.md +85 -8
- package/docs/guide/04-pages-and-regions.md +42 -3
- package/docs/guide/08-security.md +46 -5
- package/docs/guide/11-from-apex.md +4 -1
- package/docs/guide/12-development.md +13 -3
- package/docs/guide/17-mobile.md +3 -1
- package/docs/guide/18-cli.md +2 -0
- package/docs/guide/20-ai-agents.md +2 -0
- package/examples/hr/hr_40_sample_data.sql +1 -1
- package/examples/hr/hr_54_region_templates.sql +67 -0
- package/examples/hr/hr_55_page_groups.sql +24 -0
- package/package.json +1 -1
- package/public/app.css +60 -3
- package/public/app.js +89 -3
- package/public/builder.css +11 -0
- package/public/builder.js +50 -0
- package/public/code-editor.css +3 -3
- package/public/sw.js +5 -3
- package/src/app.ts +20 -0
- package/src/appfiles.ts +1 -0
- package/src/automations.ts +3 -2
- package/src/builder/advisor.ts +25 -5
- package/src/builder/ai-builder.ts +27 -18
- package/src/builder/blueprints.ts +7 -7
- package/src/builder/code-editor.ts +3 -2
- package/src/builder/components.ts +15 -2
- package/src/builder/dataload.ts +24 -14
- package/src/builder/ddl-tools.ts +117 -0
- package/src/builder/designer.ts +29 -3
- package/src/builder/documents.ts +2 -1
- package/src/builder/forms.ts +6 -4
- package/src/builder/home.ts +6 -2
- package/src/builder/login-page.ts +82 -0
- package/src/builder/newapp.ts +21 -3
- package/src/builder/quicksql.ts +3 -2
- package/src/builder/region-help.ts +4 -2
- package/src/builder/region-settings.ts +19 -3
- package/src/builder/report-settings.ts +4 -2
- package/src/builder/routes.ts +18 -6
- package/src/builder/sampledata.ts +22 -18
- package/src/builder/scripts.ts +27 -16
- package/src/builder/shared.ts +1 -1
- package/src/builder/sql.ts +18 -10
- package/src/builder/ui.ts +2 -2
- package/src/builder/unload.ts +3 -2
- package/src/builder/workspaces.ts +175 -15
- package/src/cli/main.ts +45 -0
- package/src/cli/mcp.ts +42 -0
- package/src/cli/replace.ts +4 -0
- package/src/db.ts +36 -6
- package/src/ddl.ts +510 -0
- package/src/debug.ts +3 -2
- 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/icons.ts +2 -1
- package/src/layout.ts +3 -2
- package/src/metadata.ts +10 -4
- package/src/migrate.ts +3 -0
- package/src/process-jobs.ts +7 -2
- package/src/restsync.ts +3 -2
- package/src/runtime/devbar.ts +3 -1
- package/src/runtime/display-selector.ts +12 -4
- package/src/runtime/grid.ts +170 -26
- package/src/runtime/lists.ts +11 -1
- package/src/runtime/password-reset.ts +171 -0
- package/src/runtime/pwa.ts +7 -10
- package/src/runtime/regions.ts +33 -0
- package/src/runtime/render.ts +15 -0
- package/src/runtime/report-views.ts +4 -2
- package/src/runtime/report.ts +36 -22
- package/src/runtime/routes.ts +39 -33
- package/src/runtime/signin.ts +80 -0
- package/src/sampledata.ts +3 -3
- package/src/session.ts +5 -2
- package/src/static-version.ts +34 -0
- package/src/websources.ts +3 -1
- package/src/workflow.ts +65 -9
- package/src/workspace-db.ts +166 -0
package/SECURITY.md
CHANGED
|
@@ -67,7 +67,8 @@ first version (MVP). Every finding below has a regression test in
|
|
|
67
67
|
| AI services (0.28.0) | Administrators only configure services (anonymous → sign-in, other developers 403, CSRF); API keys are encrypted with `PGKILN_SECRET_KEY`, write-only, never exported, logged or shown; all AI tables are revoked from public and the runtime role. Model, base URL and limits are checked; models are never changed silently. Item values go into prompts as escaped `<data>` text marked as data, never instructions; session ids and password items are never sent. Answers only become item values and are escaped when shown. Usage is logged without prompt or answer text (except at debug level 9). Data in prompts leaves the instance for the chosen provider. AI calls don't use `PGKILN_REST_ALLOWED_HOSTS`: only administrators set the base URL. |
|
|
68
68
|
| AI assistant, NL2IR (0.28.0) | CSRF and region visibility are checked before any AI call; assistants are for signed-in users unless marked public. Conversations belong to one session and region, are deleted with the session and are closed to the runtime and application roles. Context queries and tools run as the app's role (grants, RLS): arguments are bound literals checked against the declared parameters (unknown arguments refused), each tool is one subquery with a row limit and a 15 s timeout and is rolled back unless the developer marks it as writing; tools can be limited by an authorization scheme; REST tool arguments are never `&ITEM.`-substituted. Natural-language report filters only produce filters a user could set by hand, on visible columns. Answers are escaped (paragraphs, lists, bold, code only). A writing tool lets the model change data on the user's behalf within the role's rights: design those with care. |
|
|
69
69
|
| App Builder AI and blueprints (0.28.0) | Developer session and CSRF; only administrators choose the builder's AI service. Generated SQL is shown, never run automatically; page proposals are checked (page types, tables the app role can read, free page numbers) and created only after the developer confirms. The model sees names, types and keys, never rows. A blueprint is validated (identifiers, references, rows) and turned into quoted SQL; Create only accepts the exact text the review showed, signed with the developer's session, in one transaction. `meta.builder_ai`, `meta.ai_table_note` and `meta.blueprint` are closed to the runtime role. |
|
|
70
|
-
| Workspaces (0.29.0) | Every builder request under `/apps/:id` or `/pages/:pid` (GET and POST), a working copy's main application, a boilerplate and the code editor's application are checked against the developer's workspaces in `developer()` and answer 404 outside them; lists are filtered by the current workspace. Switching needs CSRF and membership; managing workspaces needs an administrator and CSRF; names are escaped; subscriptions only reach theme and library applications of the same workspace. Workspace tables are closed to the runtime role. **
|
|
70
|
+
| Workspaces (0.29.0) | Every builder request under `/apps/:id` or `/pages/:pid` (GET and POST), a working copy's main application, a boilerplate and the code editor's application are checked against the developer's workspaces in `developer()` and answer 404 outside them; lists are filtered by the current workspace. Switching needs CSRF and membership; managing workspaces needs an administrator and CSRF; names are escaped; subscriptions only reach theme and library applications of the same workspace. Workspace tables are closed to the runtime role. **A shared workspace is not a tenant boundary:** its SQL Workshop runs as the owner and its application code on the shared runtime connection; isolate a workspace for that (below). |
|
|
71
|
+
| Isolated workspaces (sprint 60) | The boundary is the PostgreSQL login (`session_user`, which SQL can't change): an isolated workspace's applications run on its own run login and its developers' SQL on its own developer login (`src/workspace-db.ts`; passwords derived from `PGKILN_SECRET_KEY`, never stored), so `RESET ROLE` or `set_config('role', …)` only reach that login. The run login has `pgkiln_app_code` (the runtime role's rights in `meta`, kept in step after every migration by `meta.sync_app_code_grants()`, checked by a test) and the workspace's roles; the developer login only the workspace's roles. Row-level security on every `meta` table the runtime reads keys on the login (`meta.visible_apps()`), both ways: isolated workspaces don't see others, the shared runtime doesn't see isolated ones; the owner isn't affected. `meta.app_id()` and the eight functions that take an application id (`meta.checked_app`) refuse an application the login may not see. A workspace's database roles are an administrator's list (`meta.workspace_role`): an application there may only run as one of them, a role belongs to one workspace, and superusers, roles with CREATEROLE/BYPASSRLS/REPLICATION and pgkiln's own roles are refused; memberships follow by triggers (`meta.workspace_sync_roles()`). A new application gets a new role and a new or own schema. Every builder tool that runs a developer's SQL uses the workspace's developer login (`workshopDb`, `devDbForApp`); the import check plans the imported SQL on it, outside the import's owner transaction. Saved scripts, runs, blueprints and generators are per workspace. **Not covered:** administrators and developers who also belong to a shared workspace (owner rights there), the `pg_catalog` (definitions are readable by every login), PostgREST (one authenticator), identity providers, LDAP, mail and AI services (installation-wide). |
|
|
71
72
|
| Drawers, built-in template components, Theme Roller additions (0.29.0) | Dialog positions and sizes, base styles, dark-mode colours, item and report-column template options only reach the page from fixed lists (checked when saved and again when rendered: a hand-edited value is ignored); the browser gets dialog shapes as JSON values from those lists. Built-in template components pass the same template checks as plug-ins; *Copy into this application* takes a static id from the fixed list, needs CSRF and the application in the developer's workspace, and never overwrites the application's own component. |
|
|
72
73
|
| Report row selection across pages (0.29.0) | `POST …/report/<id>/select` needs the CSRF token, page access and a visible report region whose `selection` names the item (and the item visible); it only changes that item's session state; values are bounded (5000, 400 characters, no `:`) and treated as user input, like a submit; output is escaped. |
|
|
73
74
|
| Instance settings (0.29.0) | Administrators only, with CSRF; only whole numbers within each setting's range are stored (a bad stored value falls back to the environment variable or default); the configuration overview never shows secrets (connection strings, keys: only whether they are set); changes are logged.
|
|
@@ -184,18 +185,19 @@ A second full review, mainly of what end users and API callers can reach. Regres
|
|
|
184
185
|
can do what the runtime role can: read every application's metadata and sessions, set
|
|
185
186
|
`pgkiln.app_id` / `pgkiln.app_user` and call `meta.page_url()` for any application and user (valid
|
|
186
187
|
checksums), or change sessions. Treat every developer as trusted with all applications of the
|
|
187
|
-
instance, as the workspace notes say; end users never write SQL.
|
|
188
|
-
|
|
188
|
+
instance's shared workspaces, as the workspace notes say; end users never write SQL. An isolated workspace
|
|
189
|
+
(sprint 60) closes this between workspaces: its SQL resets to a login of its own.
|
|
189
190
|
- **Administrators and developers**: SQL Commands, SQL Scripts and the other SQL Workshop tools run on
|
|
190
|
-
the owner connection for
|
|
191
|
+
the owner connection for administrators and the developers of shared workspaces, so a developer can change anything, including
|
|
191
192
|
`meta.developer.is_admin`. The administrator-only pages (instance settings, AI services, identity
|
|
192
193
|
providers, LDAP directories, the password policy, developers) prevent mistakes; they are not a
|
|
193
194
|
boundary against a developer who means harm.
|
|
194
|
-
- **App id from SQL (
|
|
195
|
-
application's own SQL can change.
|
|
196
|
-
therefore be made under another application's id, using its web credentials or
|
|
197
|
-
Page processes are not affected (they use the server's app id).
|
|
198
|
-
|
|
195
|
+
- **App id from SQL (shared workspaces)**: `meta.app_id()` reads the `pgkiln.app_id` setting, which an
|
|
196
|
+
application's own SQL can change. In shared workspaces, requests queued from SQL (`meta.web_request`,
|
|
197
|
+
`meta.ai_generate`) could therefore be made under another shared application's id, using its web credentials or
|
|
198
|
+
AI services and quota. Page processes are not affected (they use the server's app id). Since sprint 60,
|
|
199
|
+
`meta.app_id()` refuses an application outside the login's workspace: isolate a workspace whose applications must
|
|
200
|
+
not be trusted by the others.
|
|
199
201
|
- **Lockout trade-off**: per-user throttling lets an attacker lock a known
|
|
200
202
|
account for 15 minutes. The per-IP limit and the activity monitor help you
|
|
201
203
|
spot this.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 087: an editable login page and "Forgot password?" for app users
|
|
3
|
+
--
|
|
4
|
+
-- meta.app.login_page: the page that replaces the built-in sign-in page
|
|
5
|
+
-- (APEX: the login page, usually 9999). /a/<alias>/login sends visitors
|
|
6
|
+
-- there; it must not require authentication. Its "login" region is the
|
|
7
|
+
-- sign-in form, which posts to /a/<alias>/login as before.
|
|
8
|
+
-- meta.page.page_template: 'minimal' shows a page without the header and
|
|
9
|
+
-- navigation (APEX: Minimal (No Navigation)); login pages use it.
|
|
10
|
+
-- meta.app.password_reset: the sign-in page offers "Forgot password?" to
|
|
11
|
+
-- accounts with a password in the user directory, when the application
|
|
12
|
+
-- has an enabled mail service. A reset link is single-use and expires
|
|
13
|
+
-- after 30 minutes; only a SHA-256 hash of its token is stored.
|
|
14
|
+
-- meta.password_reset: the tokens (owner only; the functions below are
|
|
15
|
+
-- the runtime's way in).
|
|
16
|
+
-- =====================================================================
|
|
17
|
+
alter table meta.app add column login_page int check (login_page > 0);
|
|
18
|
+
alter table meta.app add column password_reset boolean not null default false;
|
|
19
|
+
alter table meta.page add column page_template text not null default 'standard' check (page_template in ('standard', 'minimal'));
|
|
20
|
+
|
|
21
|
+
-- an export made before 087 has neither: NULL becomes the default (like 077)
|
|
22
|
+
create function meta.login_page_defaults() returns trigger
|
|
23
|
+
language plpgsql set search_path = pg_catalog as $$
|
|
24
|
+
begin
|
|
25
|
+
if tg_table_name = 'app' then
|
|
26
|
+
new.password_reset := coalesce(new.password_reset, false);
|
|
27
|
+
else
|
|
28
|
+
new.page_template := coalesce(new.page_template, 'standard');
|
|
29
|
+
end if;
|
|
30
|
+
return new;
|
|
31
|
+
end
|
|
32
|
+
$$;
|
|
33
|
+
create trigger app_login_page_defaults before insert or update on meta.app
|
|
34
|
+
for each row execute function meta.login_page_defaults();
|
|
35
|
+
create trigger page_login_page_defaults before insert or update on meta.page
|
|
36
|
+
for each row execute function meta.login_page_defaults();
|
|
37
|
+
|
|
38
|
+
alter table meta.region drop constraint region_type_check;
|
|
39
|
+
alter table meta.region add constraint region_type_check
|
|
40
|
+
check (type in ('report', 'form', 'chart', 'cards', 'static', 'grid', 'calendar', 'dynamic', 'facets', 'tasks', 'workflows', 'map', 'tree',
|
|
41
|
+
'template_component', 'smart_filters', 'display_selector', 'list', 'data_reporter', 'ai_assistant', 'plugin', 'url', 'search',
|
|
42
|
+
'login'));
|
|
43
|
+
|
|
44
|
+
create table meta.password_reset (
|
|
45
|
+
id bigint generated always as identity primary key,
|
|
46
|
+
app_id int not null references meta.app on delete cascade,
|
|
47
|
+
account_id int not null references meta.account on delete cascade,
|
|
48
|
+
token_hash text not null unique check (token_hash ~ '^[0-9a-f]{64}$'),
|
|
49
|
+
ip text,
|
|
50
|
+
created_at timestamptz not null default now(),
|
|
51
|
+
expires_at timestamptz not null default now() + interval '30 minutes',
|
|
52
|
+
used_at timestamptz
|
|
53
|
+
);
|
|
54
|
+
create index password_reset_account on meta.password_reset (account_id, created_at);
|
|
55
|
+
revoke all on meta.password_reset from public;
|
|
56
|
+
|
|
57
|
+
-- Whether the application offers "Forgot password?": turned on, app users with local passwords,
|
|
58
|
+
-- and an enabled mail service with a From address.
|
|
59
|
+
create function meta.password_reset_available(p_app_id int) returns boolean
|
|
60
|
+
language sql stable security definer set search_path = meta, pg_catalog as $$
|
|
61
|
+
select exists (
|
|
62
|
+
select 1
|
|
63
|
+
from meta.app a
|
|
64
|
+
join meta.app_mail_service ams on ams.app_id = a.id
|
|
65
|
+
join meta.mail_service s on s.id = ams.service_id
|
|
66
|
+
where a.id = p_app_id and a.password_reset and a.authentication = 'app_users' and a.local_login
|
|
67
|
+
and s.enabled and nullif(btrim(ams.from_address), '') is not null)
|
|
68
|
+
$$;
|
|
69
|
+
revoke all on function meta.password_reset_available(int) from public;
|
|
70
|
+
grant execute on function meta.password_reset_available(int) to pgkiln_runtime;
|
|
71
|
+
|
|
72
|
+
-- A reset request for a user name or e-mail address. When it matches one active account with a
|
|
73
|
+
-- password and an e-mail address that may use the application, keeps the token's hash and queues
|
|
74
|
+
-- the mail (#USER# in the subject and bodies becomes the user name; the caller puts the link in).
|
|
75
|
+
-- Says nothing about whether an account matched: the caller shows the same answer either way.
|
|
76
|
+
-- At most 3 requests per account per hour.
|
|
77
|
+
create function meta.request_password_reset(p_app_id int, p_login text, p_token_hash text, p_ip text,
|
|
78
|
+
p_subject text, p_body text, p_body_html text) returns void
|
|
79
|
+
language plpgsql volatile security definer set search_path = meta, public, pg_catalog as $$
|
|
80
|
+
declare
|
|
81
|
+
v_acc meta.account;
|
|
82
|
+
v_n int;
|
|
83
|
+
v_from text;
|
|
84
|
+
v_send text[];
|
|
85
|
+
v_max int;
|
|
86
|
+
v_vals jsonb;
|
|
87
|
+
v_id bigint;
|
|
88
|
+
begin
|
|
89
|
+
if not meta.password_reset_available(p_app_id) or p_token_hash !~ '^[0-9a-f]{64}$' or btrim(coalesce(p_login, '')) = '' then
|
|
90
|
+
return;
|
|
91
|
+
end if;
|
|
92
|
+
-- a user name, else an e-mail address that only one active account has
|
|
93
|
+
select * into v_acc from meta.account where lower(username) = lower(btrim(p_login)) and active;
|
|
94
|
+
if not found then
|
|
95
|
+
select count(*) into v_n from meta.account where lower(email) = lower(btrim(p_login)) and active;
|
|
96
|
+
if v_n <> 1 then
|
|
97
|
+
return;
|
|
98
|
+
end if;
|
|
99
|
+
select * into v_acc from meta.account where lower(email) = lower(btrim(p_login)) and active;
|
|
100
|
+
end if;
|
|
101
|
+
if v_acc.password_hash is null or nullif(btrim(v_acc.email), '') is null then
|
|
102
|
+
return;
|
|
103
|
+
end if;
|
|
104
|
+
if not exists (select 1 from meta.app a where a.id = p_app_id and a.access_control = 'any_user')
|
|
105
|
+
and not exists (select 1 from meta.app_access where app_id = p_app_id and account_id = v_acc.id) then
|
|
106
|
+
return;
|
|
107
|
+
end if;
|
|
108
|
+
select count(*) into v_n from meta.password_reset where account_id = v_acc.id and created_at > now() - interval '1 hour';
|
|
109
|
+
if v_n >= 3 then
|
|
110
|
+
return;
|
|
111
|
+
end if;
|
|
112
|
+
select ams.from_address, s.senders, ams.max_per_day into v_from, v_send, v_max
|
|
113
|
+
from meta.app_mail_service ams join meta.mail_service s on s.id = ams.service_id
|
|
114
|
+
where ams.app_id = p_app_id;
|
|
115
|
+
if not meta.mail_sender_allowed(v_send, v_from) then
|
|
116
|
+
raise warning 'password reset for app %: the mail service may not send as %', p_app_id, v_from;
|
|
117
|
+
return;
|
|
118
|
+
end if;
|
|
119
|
+
if v_max is not null and (select count(*) from meta.mail_queue
|
|
120
|
+
where app_id = p_app_id and created_at >= date_trunc('day', now() at time zone 'UTC') at time zone 'UTC'
|
|
121
|
+
and status <> 'cancelled') >= v_max then
|
|
122
|
+
raise warning 'password reset for app %: the daily mail limit is reached', p_app_id;
|
|
123
|
+
return;
|
|
124
|
+
end if;
|
|
125
|
+
delete from meta.password_reset where created_at < now() - interval '7 days';
|
|
126
|
+
insert into meta.password_reset (app_id, account_id, token_hash, ip) values (p_app_id, v_acc.id, p_token_hash, left(p_ip, 100));
|
|
127
|
+
v_vals := jsonb_build_object('USER', v_acc.username);
|
|
128
|
+
insert into meta.mail_queue (app_id, username, from_address, to_addresses, subject, body_text, body_html, template)
|
|
129
|
+
values (p_app_id, v_acc.username, v_from, array[v_acc.email], left(replace(replace(meta.mail_fill(p_subject, v_vals, false), E'\r', ''), E'\n', ' '), 998),
|
|
130
|
+
meta.mail_fill(p_body, v_vals, false), meta.mail_fill(p_body_html, v_vals, true), 'PASSWORD_RESET')
|
|
131
|
+
returning id into v_id;
|
|
132
|
+
perform pg_notify('pgkiln_mail', v_id::text);
|
|
133
|
+
end
|
|
134
|
+
$$;
|
|
135
|
+
revoke all on function meta.request_password_reset(int, text, text, text, text, text, text) from public;
|
|
136
|
+
grant execute on function meta.request_password_reset(int, text, text, text, text, text, text) to pgkiln_runtime;
|
|
137
|
+
|
|
138
|
+
-- The user name a valid (unused, unexpired) token is for, or null.
|
|
139
|
+
create function meta.password_reset_user(p_app_id int, p_token_hash text) returns text
|
|
140
|
+
language sql stable security definer set search_path = meta, pg_catalog as $$
|
|
141
|
+
select ac.username
|
|
142
|
+
from meta.password_reset r join meta.account ac on ac.id = r.account_id
|
|
143
|
+
where r.app_id = p_app_id and r.token_hash = p_token_hash and r.used_at is null and r.expires_at > now() and ac.active
|
|
144
|
+
$$;
|
|
145
|
+
revoke all on function meta.password_reset_user(int, text) from public;
|
|
146
|
+
grant execute on function meta.password_reset_user(int, text) to pgkiln_runtime;
|
|
147
|
+
|
|
148
|
+
-- Set the new password with a valid token: the token and the account's other open requests are used
|
|
149
|
+
-- up, every session and "keep me signed in" token of the account ends. Returns the user name, or
|
|
150
|
+
-- null when the token is not valid (any more).
|
|
151
|
+
create function meta.reset_password(p_app_id int, p_token_hash text, p_new text) returns text
|
|
152
|
+
language plpgsql volatile security definer set search_path = meta, public, pg_catalog as $$
|
|
153
|
+
declare
|
|
154
|
+
v_reset meta.password_reset;
|
|
155
|
+
v_acc meta.account;
|
|
156
|
+
begin
|
|
157
|
+
select * into v_reset from meta.password_reset
|
|
158
|
+
where app_id = p_app_id and token_hash = p_token_hash and used_at is null and expires_at > now()
|
|
159
|
+
for update;
|
|
160
|
+
if not found or coalesce(p_new, '') = '' then
|
|
161
|
+
return null;
|
|
162
|
+
end if;
|
|
163
|
+
select * into v_acc from meta.account where id = v_reset.account_id and active;
|
|
164
|
+
if not found then
|
|
165
|
+
return null;
|
|
166
|
+
end if;
|
|
167
|
+
update meta.account set password_hash = crypt(p_new, gen_salt('bf', 10)), must_change_password = false where id = v_acc.id;
|
|
168
|
+
update meta.password_reset set used_at = now() where account_id = v_acc.id and used_at is null;
|
|
169
|
+
delete from meta.session where app_id is not null and lower(username) = lower(v_acc.username);
|
|
170
|
+
delete from meta.persistent_login where account_id = v_acc.id;
|
|
171
|
+
return v_acc.username;
|
|
172
|
+
end
|
|
173
|
+
$$;
|
|
174
|
+
revoke all on function meta.reset_password(int, text, text) from public;
|
|
175
|
+
grant execute on function meta.reset_password(int, text, text) to pgkiln_runtime;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 088: more region templates (APEX: Hero, Title Bar, Buttons Container,
|
|
3
|
+
-- Inline Dialog, Inline Drawer, Wizard Container)
|
|
4
|
+
--
|
|
5
|
+
-- hero a large banner: title, text, an icon, buttons
|
|
6
|
+
-- title_bar the title and buttons on one line, without a card
|
|
7
|
+
-- buttons_container the buttons only (and any text), without a header
|
|
8
|
+
-- inline_dialog hidden until opened (dynamic action "Open region", or
|
|
9
|
+
-- a link to #R<id>); a dialog over the page
|
|
10
|
+
-- inline_drawer the same, sliding in from the side
|
|
11
|
+
-- wizard a narrow card for one step of a wizard (with the list
|
|
12
|
+
-- template "wizard_progress" above it)
|
|
13
|
+
-- The carousel is a style of the display selector region ({"style":
|
|
14
|
+
-- "carousel"}); it needs no column.
|
|
15
|
+
-- =====================================================================
|
|
16
|
+
alter table meta.region drop constraint region_template_check;
|
|
17
|
+
alter table meta.region add constraint region_template_check
|
|
18
|
+
check (template in ('standard', 'plain', 'collapsible', 'hero', 'title_bar', 'buttons_container', 'inline_dialog', 'inline_drawer', 'wizard'));
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 089: page groups (APEX: Shared Components → Page Groups)
|
|
3
|
+
--
|
|
4
|
+
-- meta.page_group: an application's named groups of pages, to organise
|
|
5
|
+
-- them in the App Builder (the page list by group, a filter). A page's
|
|
6
|
+
-- group is meta.page.page_group (the group's name; renaming a group in
|
|
7
|
+
-- the builder renames it on its pages). They change nothing at run time.
|
|
8
|
+
-- export_app / import_app are redefined from 086 to carry page_groups.
|
|
9
|
+
-- =====================================================================
|
|
10
|
+
create table meta.page_group (
|
|
11
|
+
id serial primary key,
|
|
12
|
+
app_id int not null references meta.app on delete cascade,
|
|
13
|
+
name text not null check (name ~ '^[A-Za-z0-9][A-Za-z0-9 _.-]{0,59}$'),
|
|
14
|
+
description text,
|
|
15
|
+
unique (app_id, name)
|
|
16
|
+
);
|
|
17
|
+
revoke all on meta.page_group from public;
|
|
18
|
+
grant select on meta.page_group to pgkiln_runtime;
|
|
19
|
+
comment on table meta.page_group is 'Page groups (089): named groups of an application''s pages, for the App Builder';
|
|
20
|
+
|
|
21
|
+
alter table meta.page add column page_group text;
|
|
22
|
+
|
|
23
|
+
-- renaming a group renames it on its pages; deleting it takes the pages out of it
|
|
24
|
+
create function meta.page_group_follow() returns trigger
|
|
25
|
+
language plpgsql set search_path = meta, pg_catalog as $$
|
|
26
|
+
begin
|
|
27
|
+
if tg_op = 'DELETE' then
|
|
28
|
+
update meta.page set page_group = null where app_id = old.app_id and page_group = old.name;
|
|
29
|
+
return old;
|
|
30
|
+
end if;
|
|
31
|
+
if new.name is distinct from old.name then
|
|
32
|
+
update meta.page set page_group = new.name where app_id = new.app_id and page_group = old.name;
|
|
33
|
+
end if;
|
|
34
|
+
return new;
|
|
35
|
+
end
|
|
36
|
+
$$;
|
|
37
|
+
create trigger page_group_follow after update or delete on meta.page_group
|
|
38
|
+
for each row execute function meta.page_group_follow();
|
|
39
|
+
|
|
40
|
+
create or replace function meta.export_app(p_alias text) returns jsonb
|
|
41
|
+
language sql stable set search_path = meta, pg_catalog as $$
|
|
42
|
+
select jsonb_build_object(
|
|
43
|
+
'format', 'pgkiln/2',
|
|
44
|
+
-- (083) availability and build status belong to the installation
|
|
45
|
+
'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at' - 'availability' - 'availability_message' - 'availability_users' - 'build_status',
|
|
46
|
+
'authz_schemes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.authz_scheme x where x.app_id = a.id), '[]'),
|
|
47
|
+
'app_items', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.app_item x where x.app_id = a.id), '[]'),
|
|
48
|
+
'app_processes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.seq, x.id) from meta.app_process x where x.app_id = a.id), '[]'),
|
|
49
|
+
'lovs', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.lov x where x.app_id = a.id), '[]'),
|
|
50
|
+
'group_roles', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.group_name, x.role) from meta.app_group_role x where x.app_id = a.id), '[]'),
|
|
51
|
+
'text_messages', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.name, x.language) from meta.text_message x where x.app_id = a.id), '[]'),
|
|
52
|
+
'translations', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.language, x.source) from meta.translation x where x.app_id = a.id), '[]'),
|
|
53
|
+
'report_layouts', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'logo' || jsonb_build_object('logo', encode(x.logo, 'base64')) order by x.name)
|
|
54
|
+
from meta.report_layout x where x.app_id = a.id), '[]'),
|
|
55
|
+
'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
|
|
56
|
+
from meta.automation x where x.app_id = a.id), '[]'),
|
|
57
|
+
-- (044) the actions of the automations, by automation name
|
|
58
|
+
'automation_actions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.automation_name collate "C", x.seq, x.name collate "C")
|
|
59
|
+
from meta.automation_action x where x.app_id = a.id), '[]'),
|
|
60
|
+
'document_templates', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.document_template x where x.app_id = a.id), '[]'),
|
|
61
|
+
'task_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.task_definition x where x.app_id = a.id), '[]'),
|
|
62
|
+
'workflow_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.workflow_definition x where x.app_id = a.id), '[]'),
|
|
63
|
+
'rest_modules', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.rest_module x where x.app_id = a.id), '[]'),
|
|
64
|
+
'template_components', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.static_id) from meta.template_component x where x.app_id = a.id), '[]'),
|
|
65
|
+
'build_options', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.build_option x where x.app_id = a.id), '[]'),
|
|
66
|
+
-- (030) secrets never leave the installation: they are entered again after an import
|
|
67
|
+
-- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
|
|
68
|
+
'web_credentials', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at' order by x.name)
|
|
69
|
+
from meta.web_credential x where x.app_id = a.id), '[]'),
|
|
70
|
+
-- (050) the synchronisation's state (next and last run) belongs to the installation
|
|
71
|
+
'rest_sources', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'sync_next_at' - 'sync_last_at' - 'sync_last_status' order by x.name)
|
|
72
|
+
from meta.rest_source x where x.app_id = a.id), '[]'),
|
|
73
|
+
-- (041) data load definitions
|
|
74
|
+
'data_load_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.data_load_def x where x.app_id = a.id), '[]'),
|
|
75
|
+
-- (042) lists; entries keep their ids so parents can be remapped on import
|
|
76
|
+
'lists', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.list x where x.app_id = a.id), '[]'),
|
|
77
|
+
'list_entries', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.list_name, x.parent_id nulls first, x.seq, x.id) from meta.list_entry x where x.app_id = a.id), '[]'),
|
|
78
|
+
-- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
|
|
79
|
+
'supporting_scripts', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.supporting_script x where x.app_id = a.id), '[]'),
|
|
80
|
+
-- (069) plug-ins (their files are static files, their templates template components)
|
|
81
|
+
'plugins', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.plugin x where x.app_id = a.id), '[]'),
|
|
82
|
+
-- (080) search configurations
|
|
83
|
+
'search_configs', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.search_config x where x.app_id = a.id), '[]'),
|
|
84
|
+
-- (081) e-mail templates
|
|
85
|
+
'email_templates', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.email_template x where x.app_id = a.id), '[]'),
|
|
86
|
+
-- (086) application settings (with their values) and dynamic translations
|
|
87
|
+
'app_settings', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.app_setting x where x.app_id = a.id), '[]'),
|
|
88
|
+
'page_groups', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.page_group x where x.app_id = a.id), '[]'),
|
|
89
|
+
'dynamic_translations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.language, x.source collate "C") from meta.dynamic_translation x where x.app_id = a.id), '[]'),
|
|
90
|
+
-- (068) static application files (JavaScript, CSS, images, fonts) as base64
|
|
91
|
+
'static_files', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'content' - 'updated_at' || jsonb_build_object('content', translate(encode(x.content, 'base64'), E'\n', '')) order by x.name)
|
|
92
|
+
from meta.static_file x where x.app_id = a.id), '[]'),
|
|
93
|
+
-- nav entries and regions keep their ids, so parents and references can be remapped on import
|
|
94
|
+
'nav', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.parent_id nulls first, x.seq, x.id) from meta.nav_entry x where x.app_id = a.id), '[]'),
|
|
95
|
+
'pages', coalesce((
|
|
96
|
+
select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
|
|
97
|
+
'regions', coalesce((select jsonb_agg(to_jsonb(r) - 'page_id' order by r.seq, r.id) from meta.region r where r.page_id = p.id), '[]'),
|
|
98
|
+
'items', coalesce((select jsonb_agg(to_jsonb(i) - 'id' - 'page_id' order by i.seq, i.id) from meta.item i where i.page_id = p.id), '[]'),
|
|
99
|
+
'buttons', coalesce((select jsonb_agg(to_jsonb(b) - 'id' - 'page_id' order by b.seq, b.id) from meta.button b where b.page_id = p.id), '[]'),
|
|
100
|
+
'dynamic_actions', coalesce((select jsonb_agg(to_jsonb(d) - 'id' - 'page_id' order by d.seq, d.id) from meta.dynamic_action d where d.page_id = p.id), '[]'),
|
|
101
|
+
'validations', coalesce((select jsonb_agg(to_jsonb(v) - 'id' - 'page_id' order by v.seq, v.id) from meta.validation v where v.page_id = p.id), '[]'),
|
|
102
|
+
'processes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.process x where x.page_id = p.id), '[]'),
|
|
103
|
+
'computations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.computation x where x.page_id = p.id), '[]'),
|
|
104
|
+
'branches', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.branch x where x.page_id = p.id), '[]')
|
|
105
|
+
) order by p.page_no)
|
|
106
|
+
from meta.page p where p.app_id = a.id), '[]'))
|
|
107
|
+
from meta.app a
|
|
108
|
+
where a.alias = p_alias
|
|
109
|
+
$$;
|
|
110
|
+
|
|
111
|
+
-- ---------------------------------------------------------------- import
|
|
112
|
+
-- Same as 081, plus app_settings and dynamic_translations.
|
|
113
|
+
|
|
114
|
+
create or replace function meta.import_app(p_doc jsonb, p_alias text default null) returns int
|
|
115
|
+
language plpgsql set search_path = meta, pg_catalog as $$
|
|
116
|
+
declare
|
|
117
|
+
v_app_id int;
|
|
118
|
+
v_page_id int;
|
|
119
|
+
v_page jsonb;
|
|
120
|
+
v_e jsonb;
|
|
121
|
+
v_rmap jsonb;
|
|
122
|
+
v_nmap jsonb := '{}';
|
|
123
|
+
v_lmap jsonb := '{}';
|
|
124
|
+
v_new_id int;
|
|
125
|
+
begin
|
|
126
|
+
if p_doc->>'format' is distinct from 'pgkiln/2' then
|
|
127
|
+
raise exception 'unsupported export format %', coalesce(p_doc->>'format', '(none)');
|
|
128
|
+
end if;
|
|
129
|
+
|
|
130
|
+
insert into meta.app
|
|
131
|
+
select (jsonb_populate_record(null::meta.app, p_doc->'app' || jsonb_build_object(
|
|
132
|
+
'id', nextval('meta.app_id_seq'),
|
|
133
|
+
'alias', coalesce(p_alias, p_doc->'app'->>'alias'),
|
|
134
|
+
'created_at', now(), 'updated_at', now()))).*
|
|
135
|
+
returning id into v_app_id;
|
|
136
|
+
|
|
137
|
+
insert into meta.authz_scheme
|
|
138
|
+
select (jsonb_populate_record(null::meta.authz_scheme, e || jsonb_build_object('id', nextval('meta.authz_scheme_id_seq'), 'app_id', v_app_id))).*
|
|
139
|
+
from jsonb_array_elements(coalesce(p_doc->'authz_schemes', '[]')) e;
|
|
140
|
+
insert into meta.app_item
|
|
141
|
+
select (jsonb_populate_record(null::meta.app_item, e || jsonb_build_object('id', nextval('meta.app_item_id_seq'), 'app_id', v_app_id))).*
|
|
142
|
+
from jsonb_array_elements(coalesce(p_doc->'app_items', '[]')) e;
|
|
143
|
+
insert into meta.app_process
|
|
144
|
+
select (jsonb_populate_record(null::meta.app_process, e || jsonb_build_object('id', nextval('meta.app_process_id_seq'), 'app_id', v_app_id))).*
|
|
145
|
+
from jsonb_array_elements(coalesce(p_doc->'app_processes', '[]')) e;
|
|
146
|
+
insert into meta.lov
|
|
147
|
+
select (jsonb_populate_record(null::meta.lov, e || jsonb_build_object('id', nextval('meta.lov_id_seq'), 'app_id', v_app_id))).*
|
|
148
|
+
from jsonb_array_elements(coalesce(p_doc->'lovs', '[]')) e;
|
|
149
|
+
insert into meta.app_group_role
|
|
150
|
+
select (jsonb_populate_record(null::meta.app_group_role, e || jsonb_build_object('app_id', v_app_id))).*
|
|
151
|
+
from jsonb_array_elements(coalesce(p_doc->'group_roles', '[]')) e;
|
|
152
|
+
insert into meta.text_message
|
|
153
|
+
select (jsonb_populate_record(null::meta.text_message, e || jsonb_build_object('app_id', v_app_id))).*
|
|
154
|
+
from jsonb_array_elements(coalesce(p_doc->'text_messages', '[]')) e;
|
|
155
|
+
insert into meta.translation
|
|
156
|
+
select (jsonb_populate_record(null::meta.translation, e || jsonb_build_object('app_id', v_app_id))).*
|
|
157
|
+
from jsonb_array_elements(coalesce(p_doc->'translations', '[]')) e;
|
|
158
|
+
insert into meta.report_layout
|
|
159
|
+
select (jsonb_populate_record(null::meta.report_layout, (e - 'logo') || jsonb_build_object(
|
|
160
|
+
'id', nextval('meta.report_layout_id_seq'), 'app_id', v_app_id,
|
|
161
|
+
'logo', null))).*
|
|
162
|
+
from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e;
|
|
163
|
+
update meta.report_layout l
|
|
164
|
+
set logo = decode(e->>'logo', 'base64')
|
|
165
|
+
from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e
|
|
166
|
+
where l.app_id = v_app_id and l.name = e->>'name' and e->>'logo' is not null;
|
|
167
|
+
-- imported automations start switched off: the copy must not run the original's jobs unasked.
|
|
168
|
+
-- (044) a file with actions: the automations' code is ignored; an older file: code becomes the single action
|
|
169
|
+
insert into meta.automation
|
|
170
|
+
select (jsonb_populate_record(null::meta.automation, '{"error_handling": "stop"}'::jsonb
|
|
171
|
+
|| jsonb_strip_nulls(case when p_doc ? 'automation_actions' then e - 'code' else e end)
|
|
172
|
+
|| jsonb_build_object('id', nextval('meta.automation_id_seq'), 'app_id', v_app_id, 'enabled', false))).*
|
|
173
|
+
from jsonb_array_elements(coalesce(p_doc->'automations', '[]')) e;
|
|
174
|
+
insert into meta.automation_action
|
|
175
|
+
select (jsonb_populate_record(null::meta.automation_action, '{"seq": 10}'::jsonb || jsonb_strip_nulls(e)
|
|
176
|
+
|| jsonb_build_object('id', nextval('meta.automation_action_id_seq'), 'app_id', v_app_id))).*
|
|
177
|
+
from jsonb_array_elements(coalesce(p_doc->'automation_actions', '[]')) e;
|
|
178
|
+
insert into meta.document_template
|
|
179
|
+
select (jsonb_populate_record(null::meta.document_template, e || jsonb_build_object('id', nextval('meta.document_template_id_seq'), 'app_id', v_app_id))).*
|
|
180
|
+
from jsonb_array_elements(coalesce(p_doc->'document_templates', '[]')) e;
|
|
181
|
+
insert into meta.task_definition
|
|
182
|
+
select (jsonb_populate_record(null::meta.task_definition, e || jsonb_build_object('id', nextval('meta.task_definition_id_seq'), 'app_id', v_app_id))).*
|
|
183
|
+
from jsonb_array_elements(coalesce(p_doc->'task_definitions', '[]')) e;
|
|
184
|
+
insert into meta.workflow_definition
|
|
185
|
+
select (jsonb_populate_record(null::meta.workflow_definition, e || jsonb_build_object('id', nextval('meta.workflow_definition_id_seq'), 'app_id', v_app_id))).*
|
|
186
|
+
from jsonb_array_elements(coalesce(p_doc->'workflow_definitions', '[]')) e;
|
|
187
|
+
insert into meta.rest_module
|
|
188
|
+
select (jsonb_populate_record(null::meta.rest_module, e || jsonb_build_object('id', nextval('meta.rest_module_id_seq'), 'app_id', v_app_id))).*
|
|
189
|
+
from jsonb_array_elements(coalesce(p_doc->'rest_modules', '[]')) e;
|
|
190
|
+
-- (028) template components: regions and report columns refer to them by static id
|
|
191
|
+
insert into meta.template_component
|
|
192
|
+
select (jsonb_populate_record(null::meta.template_component, e || jsonb_build_object('id', nextval('meta.template_component_id_seq'), 'app_id', v_app_id))).*
|
|
193
|
+
from jsonb_array_elements(coalesce(p_doc->'template_components', '[]')) e;
|
|
194
|
+
-- (029) build options (components name them by name)
|
|
195
|
+
insert into meta.build_option
|
|
196
|
+
select (jsonb_populate_record(null::meta.build_option, e || jsonb_build_object('id', nextval('meta.build_option_id_seq'), 'app_id', v_app_id))).*
|
|
197
|
+
from jsonb_array_elements(coalesce(p_doc->'build_options', '[]')) e;
|
|
198
|
+
-- (030) web credentials arrive without a secret, whatever the document holds
|
|
199
|
+
-- (050) nor a password or refresh token
|
|
200
|
+
insert into meta.web_credential
|
|
201
|
+
select (jsonb_populate_record(null::meta.web_credential, '{"type": "basic", "valid_for": [], "grant_type": "client_credentials"}'::jsonb
|
|
202
|
+
|| jsonb_strip_nulls(e - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at')
|
|
203
|
+
|| jsonb_build_object('id', nextval('meta.web_credential_id_seq'), 'app_id', v_app_id))).*
|
|
204
|
+
from jsonb_array_elements(coalesce(p_doc->'web_credentials', '[]')) e;
|
|
205
|
+
-- (050) write-back operations and synchronisation; an imported synchronisation starts switched off
|
|
206
|
+
insert into meta.rest_source
|
|
207
|
+
select (jsonb_populate_record(null::meta.rest_source, '{"method": "GET", "headers": {}, "params": [], "columns": [], "cache_seconds": 0, "timeout_s": 10, "max_rows": 1000,
|
|
208
|
+
"operations": {}, "key_columns": [], "sync_mode": "merge", "sync_delete": false, "sync_time_zone": "UTC"}'::jsonb
|
|
209
|
+
|| jsonb_strip_nulls(e - 'sync_next_at' - 'sync_last_at' - 'sync_last_status')
|
|
210
|
+
|| jsonb_build_object('id', nextval('meta.rest_source_id_seq'), 'app_id', v_app_id, 'sync_enabled', false))).*
|
|
211
|
+
from jsonb_array_elements(coalesce(p_doc->'rest_sources', '[]')) e;
|
|
212
|
+
|
|
213
|
+
-- (041) data load definitions (the data_load process names them)
|
|
214
|
+
insert into meta.data_load_def
|
|
215
|
+
select (jsonb_populate_record(null::meta.data_load_def, '{"format": "auto", "headers": true, "mode": "append", "skip_errors": false, "columns": []}'::jsonb
|
|
216
|
+
|| jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.data_load_def_id_seq'), 'app_id', v_app_id))).*
|
|
217
|
+
from jsonb_array_elements(coalesce(p_doc->'data_load_definitions', '[]')) e;
|
|
218
|
+
|
|
219
|
+
-- (042) lists and their entries, supporting scripts. Entries are inserted
|
|
220
|
+
-- without a parent first and the parents set afterwards, so the order of
|
|
221
|
+
-- the entries in the document doesn't matter (a moved entry may have a
|
|
222
|
+
-- higher id than its children).
|
|
223
|
+
insert into meta.list
|
|
224
|
+
select (jsonb_populate_record(null::meta.list, '{"type": "static"}'::jsonb || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.list_id_seq'), 'app_id', v_app_id))).*
|
|
225
|
+
from jsonb_array_elements(coalesce(p_doc->'lists', '[]')) e;
|
|
226
|
+
for v_e in select * from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) loop
|
|
227
|
+
insert into meta.list_entry
|
|
228
|
+
select (jsonb_populate_record(null::meta.list_entry, '{"target_items": {}}'::jsonb || jsonb_strip_nulls(v_e) || jsonb_build_object(
|
|
229
|
+
'id', nextval('meta.list_entry_id_seq'), 'app_id', v_app_id, 'parent_id', null))).*
|
|
230
|
+
returning id into v_new_id;
|
|
231
|
+
if v_e->>'id' is not null then
|
|
232
|
+
v_lmap := v_lmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
233
|
+
end if;
|
|
234
|
+
end loop;
|
|
235
|
+
update meta.list_entry x
|
|
236
|
+
set parent_id = (v_lmap->>(e->>'parent_id'))::int
|
|
237
|
+
from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) e
|
|
238
|
+
where e->>'parent_id' is not null and v_lmap ? (e->>'parent_id') and v_lmap ? (e->>'id')
|
|
239
|
+
and x.id = (v_lmap->>(e->>'id'))::int;
|
|
240
|
+
insert into meta.supporting_script
|
|
241
|
+
select (jsonb_populate_record(null::meta.supporting_script, '{"kind": "install", "seq": 10, "script": ""}'::jsonb || jsonb_strip_nulls(e)
|
|
242
|
+
|| jsonb_build_object('id', nextval('meta.supporting_script_id_seq'), 'app_id', v_app_id))).*
|
|
243
|
+
from jsonb_array_elements(coalesce(p_doc->'supporting_scripts', '[]')) e;
|
|
244
|
+
|
|
245
|
+
-- (069) plug-ins
|
|
246
|
+
insert into meta.plugin
|
|
247
|
+
select (jsonb_populate_record(null::meta.plugin, e || jsonb_build_object('id', nextval('meta.plugin_id_seq'), 'app_id', v_app_id))).*
|
|
248
|
+
from jsonb_array_elements(coalesce(p_doc->'plugins', '[]')) e;
|
|
249
|
+
|
|
250
|
+
-- (080) search configurations (search regions name them)
|
|
251
|
+
insert into meta.search_config
|
|
252
|
+
select (jsonb_populate_record(null::meta.search_config, '{"seq": 10, "source_type": "table", "searchable_columns": [], "max_results": 10}'::jsonb
|
|
253
|
+
|| jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.search_config_id_seq'), 'app_id', v_app_id))).*
|
|
254
|
+
from jsonb_array_elements(coalesce(p_doc->'search_configs', '[]')) e;
|
|
255
|
+
|
|
256
|
+
-- (081) e-mail templates (meta.send_mail_template and the send_email process name them)
|
|
257
|
+
insert into meta.email_template
|
|
258
|
+
select (jsonb_populate_record(null::meta.email_template, e || jsonb_build_object('id', nextval('meta.email_template_id_seq'), 'app_id', v_app_id))).*
|
|
259
|
+
from jsonb_array_elements(coalesce(p_doc->'email_templates', '[]')) e;
|
|
260
|
+
|
|
261
|
+
-- (089) page groups
|
|
262
|
+
insert into meta.page_group
|
|
263
|
+
select (jsonb_populate_record(null::meta.page_group, e || jsonb_build_object('id', nextval('meta.page_group_id_seq'), 'app_id', v_app_id))).*
|
|
264
|
+
from jsonb_array_elements(coalesce(p_doc->'page_groups', '[]')) e;
|
|
265
|
+
|
|
266
|
+
-- (086) application settings and dynamic translations
|
|
267
|
+
insert into meta.app_setting
|
|
268
|
+
select (jsonb_populate_record(null::meta.app_setting, e || jsonb_build_object('id', nextval('meta.app_setting_id_seq'), 'app_id', v_app_id))).*
|
|
269
|
+
from jsonb_array_elements(coalesce(p_doc->'app_settings', '[]')) e;
|
|
270
|
+
insert into meta.dynamic_translation
|
|
271
|
+
select (jsonb_populate_record(null::meta.dynamic_translation, e || jsonb_build_object('id', nextval('meta.dynamic_translation_id_seq'), 'app_id', v_app_id))).*
|
|
272
|
+
from jsonb_array_elements(coalesce(p_doc->'dynamic_translations', '[]')) e;
|
|
273
|
+
|
|
274
|
+
-- (068) static application files
|
|
275
|
+
insert into meta.static_file (app_id, name, mime, content, updated_at)
|
|
276
|
+
select v_app_id, e->>'name', e->>'mime', decode(coalesce(e->>'content', ''), 'base64'), now()
|
|
277
|
+
from jsonb_array_elements(coalesce(p_doc->'static_files', '[]')) e;
|
|
278
|
+
|
|
279
|
+
for v_e in select * from jsonb_array_elements(coalesce(p_doc->'nav', '[]')) loop
|
|
280
|
+
insert into meta.nav_entry
|
|
281
|
+
select (jsonb_populate_record(null::meta.nav_entry, v_e || jsonb_build_object(
|
|
282
|
+
'id', nextval('meta.nav_entry_id_seq'), 'app_id', v_app_id,
|
|
283
|
+
'parent_id', v_nmap->>(v_e->>'parent_id')))).*
|
|
284
|
+
returning id into v_new_id;
|
|
285
|
+
v_nmap := v_nmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
286
|
+
end loop;
|
|
287
|
+
|
|
288
|
+
for v_page in select * from jsonb_array_elements(coalesce(p_doc->'pages', '[]')) loop
|
|
289
|
+
insert into meta.page
|
|
290
|
+
select (jsonb_populate_record(null::meta.page, v_page || jsonb_build_object('id', nextval('meta.page_id_seq'), 'app_id', v_app_id))).*
|
|
291
|
+
returning id into v_page_id;
|
|
292
|
+
|
|
293
|
+
v_rmap := '{}';
|
|
294
|
+
for v_e in select * from jsonb_array_elements(coalesce(v_page->'regions', '[]')) loop
|
|
295
|
+
insert into meta.region
|
|
296
|
+
select (jsonb_populate_record(null::meta.region, v_e || jsonb_build_object('id', nextval('meta.region_id_seq'), 'page_id', v_page_id))).*
|
|
297
|
+
returning id into v_new_id;
|
|
298
|
+
v_rmap := v_rmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
299
|
+
end loop;
|
|
300
|
+
-- facet and map regions point at their report region by id
|
|
301
|
+
update meta.region r
|
|
302
|
+
set config = jsonb_set(r.config, '{report}', to_jsonb((v_rmap->>(r.config->>'report'))::int))
|
|
303
|
+
where r.page_id = v_page_id and r.type in ('facets', 'smart_filters', 'map') and v_rmap ? (r.config->>'report');
|
|
304
|
+
|
|
305
|
+
insert into meta.item
|
|
306
|
+
select (jsonb_populate_record(null::meta.item, e || jsonb_build_object('id', nextval('meta.item_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
|
|
307
|
+
from jsonb_array_elements(coalesce(v_page->'items', '[]')) e;
|
|
308
|
+
insert into meta.button
|
|
309
|
+
select (jsonb_populate_record(null::meta.button, e || jsonb_build_object('id', nextval('meta.button_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
|
|
310
|
+
from jsonb_array_elements(coalesce(v_page->'buttons', '[]')) e;
|
|
311
|
+
insert into meta.dynamic_action
|
|
312
|
+
select (jsonb_populate_record(null::meta.dynamic_action, e || jsonb_build_object('id', nextval('meta.dynamic_action_id_seq'), 'page_id', v_page_id, 'affected_region_id', v_rmap->>(e->>'affected_region_id')))).*
|
|
313
|
+
from jsonb_array_elements(coalesce(v_page->'dynamic_actions', '[]')) e;
|
|
314
|
+
insert into meta.validation
|
|
315
|
+
select (jsonb_populate_record(null::meta.validation, e || jsonb_build_object('id', nextval('meta.validation_id_seq'), 'page_id', v_page_id))).*
|
|
316
|
+
from jsonb_array_elements(coalesce(v_page->'validations', '[]')) e;
|
|
317
|
+
insert into meta.process
|
|
318
|
+
select (jsonb_populate_record(null::meta.process, e || jsonb_build_object('id', nextval('meta.process_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
|
|
319
|
+
from jsonb_array_elements(coalesce(v_page->'processes', '[]')) e;
|
|
320
|
+
insert into meta.computation
|
|
321
|
+
select (jsonb_populate_record(null::meta.computation, e || jsonb_build_object('id', nextval('meta.computation_id_seq'), 'page_id', v_page_id))).*
|
|
322
|
+
from jsonb_array_elements(coalesce(v_page->'computations', '[]')) e;
|
|
323
|
+
insert into meta.branch
|
|
324
|
+
select (jsonb_populate_record(null::meta.branch, e || jsonb_build_object('id', nextval('meta.branch_id_seq'), 'page_id', v_page_id))).*
|
|
325
|
+
from jsonb_array_elements(coalesce(v_page->'branches', '[]')) e;
|
|
326
|
+
end loop;
|
|
327
|
+
|
|
328
|
+
return v_app_id;
|
|
329
|
+
end
|
|
330
|
+
$$;
|