pgkiln 0.31.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SECURITY.md +10 -0
- package/db/migrations/076_form_lost_update.sql +50 -0
- package/db/migrations/077_warn_unsaved.sql +41 -0
- package/db/migrations/078_url_region.sql +12 -0
- package/db/migrations/079_developer_toolbar.sql +53 -0
- package/db/migrations/080_search.sql +326 -0
- package/db/migrations/081_mail.sql +621 -0
- package/db/migrations/082_app_backups.sql +73 -0
- package/db/migrations/083_availability.sql +156 -0
- package/db/migrations/084_feedback.sql +46 -0
- package/db/migrations/085_page_behaviour.sql +65 -0
- package/db/migrations/086_data_settings.sql +599 -0
- 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/docs/apex-feature-parity.md +61 -39
- package/docs/guide/01-installation.md +1 -0
- package/docs/guide/03-builder.md +154 -4
- package/docs/guide/04-pages-and-regions.md +157 -3
- package/docs/guide/05-items.md +47 -0
- package/docs/guide/06-processing.md +210 -2
- package/docs/guide/07-dynamic-actions.md +60 -2
- package/docs/guide/08-security.md +56 -4
- package/docs/guide/09-reference.md +21 -0
- package/docs/guide/11-from-apex.md +9 -2
- package/docs/guide/12-development.md +40 -2
- package/docs/guide/14-globalization.md +15 -0
- package/docs/guide/15-extensions.md +1 -1
- package/docs/guide/17-mobile.md +3 -1
- package/docs/guide/18-cli.md +32 -1
- package/docs/guide/20-ai-agents.md +5 -2
- package/examples/hr/hr_50_search.sql +47 -0
- package/examples/hr/hr_51_mail.sql +70 -0
- package/examples/hr/hr_52_feedback.sql +15 -0
- package/examples/hr/hr_53_data_settings.sql +108 -0
- package/examples/hr/hr_54_region_templates.sql +67 -0
- package/examples/hr/hr_55_page_groups.sql +24 -0
- package/package.json +3 -1
- package/public/app.css +122 -3
- package/public/app.js +496 -13
- package/public/builder.css +22 -0
- package/public/builder.js +77 -0
- package/public/code-editor.css +3 -3
- package/public/sw.js +5 -3
- package/src/app.ts +10 -0
- package/src/appfiles.ts +13 -0
- package/src/automations.ts +12 -0
- package/src/builder/advisor.ts +68 -14
- package/src/builder/arrange.ts +2 -2
- package/src/builder/availability.ts +74 -0
- package/src/builder/backups.ts +134 -0
- package/src/builder/components.ts +123 -16
- package/src/builder/ddl-tools.ts +114 -0
- package/src/builder/designer.ts +35 -4
- package/src/builder/feedback.ts +82 -0
- package/src/builder/forms.ts +10 -5
- package/src/builder/home.ts +3 -0
- package/src/builder/locks.ts +15 -5
- package/src/builder/login-page.ts +82 -0
- package/src/builder/mail.ts +425 -0
- package/src/builder/newapp.ts +1 -1
- package/src/builder/region-help.ts +73 -0
- package/src/builder/region-settings.ts +19 -3
- package/src/builder/routes.ts +60 -5
- package/src/builder/search.ts +4 -4
- package/src/builder/shared.ts +3 -2
- package/src/builder/sql.ts +3 -0
- package/src/builder/ui.ts +9 -3
- package/src/cli/apps.ts +46 -5
- package/src/cli/main.ts +66 -5
- package/src/cli/mcp.ts +58 -4
- package/src/cli/replace.ts +40 -4
- package/src/ddl.ts +509 -0
- package/src/geocode.ts +44 -0
- package/src/i18n/ar.ts +56 -0
- package/src/i18n/cs.ts +56 -0
- package/src/i18n/da.ts +56 -0
- package/src/i18n/de.ts +56 -0
- package/src/i18n/el.ts +56 -0
- package/src/i18n/es.ts +56 -0
- package/src/i18n/fi.ts +56 -0
- package/src/i18n/fr.ts +56 -0
- package/src/i18n/he.ts +56 -0
- package/src/i18n/it.ts +56 -0
- package/src/i18n/ja.ts +56 -0
- package/src/i18n/ko.ts +56 -0
- package/src/i18n/nb.ts +56 -0
- package/src/i18n/pl.ts +56 -0
- package/src/i18n/pt.ts +56 -0
- package/src/i18n/ru.ts +56 -0
- package/src/i18n/sv.ts +56 -0
- package/src/i18n/tr.ts +56 -0
- package/src/i18n/uk.ts +56 -0
- package/src/i18n/zh.ts +56 -0
- package/src/i18n.ts +112 -0
- package/src/icons.ts +2 -1
- package/src/layout.ts +3 -2
- package/src/mail/mail.ts +148 -0
- package/src/mail/providers.ts +444 -0
- package/src/metadata.ts +34 -10
- package/src/runtime/ajax.ts +97 -0
- package/src/runtime/authz.ts +1 -1
- package/src/runtime/availability.ts +37 -0
- package/src/runtime/context.ts +46 -6
- package/src/runtime/data-export.ts +94 -0
- package/src/runtime/data-items.ts +194 -0
- package/src/runtime/devbar.ts +184 -0
- package/src/runtime/display-selector.ts +12 -4
- package/src/runtime/engine.ts +26 -1
- package/src/runtime/feedback.ts +128 -0
- package/src/runtime/grid.ts +170 -26
- package/src/runtime/items.ts +8 -0
- package/src/runtime/lists.ts +11 -1
- package/src/runtime/password-reset.ts +171 -0
- package/src/runtime/processes.ts +83 -2
- package/src/runtime/pwa.ts +7 -10
- package/src/runtime/regions.ts +41 -0
- package/src/runtime/render.ts +48 -7
- package/src/runtime/report-views.ts +4 -2
- package/src/runtime/report.ts +36 -22
- package/src/runtime/rest.ts +4 -0
- package/src/runtime/routes.ts +94 -37
- package/src/runtime/search.ts +252 -0
- package/src/runtime/signin.ts +80 -0
- package/src/runtime/url-region.ts +93 -0
- package/src/security.ts +11 -1
- package/src/server.ts +2 -0
- package/src/session.ts +8 -2
- package/src/static-version.ts +34 -0
- package/src/workingcopy.ts +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ them.
|
|
|
17
17
|
> pgkiln is an independent open source project and is not affiliated with Oracle. Oracle and APEX are trademarks of Oracle.
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img src="docs/images/builder-page-designer.png" alt="The page designer: component tree, layout and property editor" width="100%">
|
|
20
|
+
<img src="https://raw.githubusercontent.com/pgkiln/pgkiln/main/docs/images/builder-page-designer.png" alt="The page designer: component tree, layout and property editor" width="100%">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
## Screenshots
|
|
@@ -37,7 +37,7 @@ screenshots are made by `npm run screenshots`.
|
|
|
37
37
|
| **App Builder** home | **SQL Workshop**: object browser with RLS and grants |
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<img src="docs/images/app-phone.png" alt="The dashboard on a phone" width="260"><br>
|
|
40
|
+
<img src="https://raw.githubusercontent.com/pgkiln/pgkiln/main/docs/images/app-phone.png" alt="The dashboard on a phone" width="260"><br>
|
|
41
41
|
<em>Responsive on phones and tablets, and installable as a Progressive Web App</em>
|
|
42
42
|
</p>
|
|
43
43
|
|
package/SECURITY.md
CHANGED
|
@@ -76,6 +76,16 @@ first version (MVP). Every finding below has a regression test in
|
|
|
76
76
|
| Plug-ins (0.31.0) | Only developers (session, CSRF, workspace guard, app locks) import, remove and install plug-ins; `meta.import_plugin` is not executable by the runtime role, which can only read `meta.plugin`. Plug-in files are checked before anything is stored: type, names, static file names and types, base64 contents, attributes, a region's template against the template allow-list, a process function as a `schema.function` identifier (checked again by the database). Install SQL never runs on import: a developer reads and runs it as the application's role, in one transaction. Pages get only a plug-in's name and attribute values (escaped JSON in `data-plugin-attrs`, or in the page's JSON), and only for a plug-in of the matching type; its JavaScript comes from same-origin static files. A plug-in's code runs with the application's rights: install only what you trust. |
|
|
77
77
|
| Zips and parsing in SQL (0.31.0) | `meta.unpacked_file`/`meta.unpacked_entry` are closed to applications; the functions that read them are security definer and only return entries of content the caller already holds (looked up by its SHA-256). The server unpacks only zip/xlsx it receives, at most 2,000 files and 200 MB (declared sizes checked before inflating, actual sizes after), kept 24 hours. Zip entry names are relative paths (no `..`, no leading `/`). XML with a DTD or entity declarations is refused; a row selector is an element name or path, never XPath. |
|
|
78
78
|
| Object storage (0.31.0) | Bucket requests go through the web client: allow-list, private-address checks at connect time, no secrets on cross-origin redirects; the credential (type `aws_sigv4`, secret encrypted) must be valid for the bucket URL and is used only for object storage. Keys are generated by the server (`<prefix><uuid>/<cleaned name>`); prefixes can't contain `..`. Downloads keep the checksum, page access and RLS checks (the row is read as the app's role before the object is fetched) and the sandboxing headers; users never see the bucket or credential. |
|
|
79
|
+
| Lost update detection (sprint 50) | `meta.row_version()` runs with the caller's rights (the app's role: grants and RLS apply); the key is a typed parameter and column names are quoted identifiers. A posted row version that is not an MD5 hex string is ignored; changing it only weakens the user's own protection. |
|
|
80
|
+
| URL region (sprint 50) | The origin must be written out in the source (https, http only for localhost, or a path on this server; no user:password, no substitution before the path); `&ITEM.` values are URL-encoded and the result is checked to have the same origin. The CSP gets `frame-src` for exactly the origins of the page's visible URL regions; the frame is sandboxed (`allow-top-navigation` is never offered) with `referrerpolicy=strict-origin-when-cross-origin`. |
|
|
81
|
+
| Search region (sprint 50) | The search text is a query parameter (`ILIKE … ESCAPE`, wildcards escaped, at most 8 words of 200 characters); configuration column names are checked and quoted, table sources resolved with `regclass`; queries run as the app's role in savepoints. Only the application's own configurations count; results link with checksums; list sources show only entries the user may see. The region endpoint serves search regions only after page access and the region's checks. |
|
|
82
|
+
| Developer toolbar (sprint 50) | Shown only when the cookie `pgkiln_devbar` (path `/a`, HttpOnly, SameSite=Lax; a random token whose SHA-256 is on the builder session, separate from the builder's token) belongs to a live builder session of a developer who may open the application (administrator or workspace member); otherwise its routes are 404. Its POSTs need the app session's CSRF token. The viewer never shows password items. Debug per session is set only by the toolbar (`__DEBUG_LEVEL` can't be posted or put in a URL), and `meta.debug_save` stores a session's log only for a known level of a session of that application. |
|
|
83
|
+
| E-mail (sprint 50) | The mail tables and the internal functions are closed to application and runtime roles. `meta.send_mail` and friends act for `meta.app_id()` only when the caller's role (`current_setting('role')`, unchanged by SECURITY DEFINER) is that application's database role, so application code can't send in another application's name or after `RESET ROLE`. Addresses are plain addresses (no CR/LF, no display names), the subject one line, at most 50 recipients, bodies 1 MB, attachments 20 × 10 MB per mail and only for a mail queued in the same transaction; From must be one of the service's allowed senders; a daily limit per application. Template placeholders are filled in one pass and HTML-escaped unless `!RAW`. Service secrets are AES-256-GCM encrypted with `PGKILN_SECRET_KEY`, write-only, never exported, logged or put in error messages; only administrators manage services. nodemailer runs with file and URL access off. |
|
|
84
|
+
| Checked imports (sprint 51) | The Advisor runs in the import's transaction inside a savepoint that is rolled back (its `SET LOCAL ROLE` and settings with it): SQL is only planned (`EXPLAIN` without ANALYZE), PL/pgSQL blocks compiled into a `pg_temp` function, nothing is executed. A refused import rolls back entirely, its backup included. |
|
|
85
|
+
| Backups (sprint 51) | `meta.app_backup` and `meta.backup_app` are closed to the runtime and application roles; the builder serves a backup only under its own application's URL (developer session, CSRF for restore and delete, the application's lock and Run only respected for a restore). A backup is an export: it holds no secrets. |
|
|
86
|
+
| Availability and Run only (sprint 51) | Checked on every page request, file download, REST module call and, through `meta.api_check`, every PostgREST request; "developers only" needs the developer toolbar's token of a live builder session (never an API token); restricted compares user names case-insensitively. Run only is enforced on the server for every builder POST of the application (423 for JSON), except its availability, backups, locks, comments and feedback answers. |
|
|
87
|
+
| Feedback (sprint 51) | Needs the application's setting, the page's access rules and the session's CSRF token; at most 10 per session and hour, 4000 characters, escaped everywhere; users see only their own feedback; the runtime role may only insert and read `meta.feedback`; answers need a developer session and are matched to the application in the URL. |
|
|
88
|
+
| Page behaviour (sprint 52) | Ajax callbacks need the CSRF token and page access, run only processes with point *ajax* of this page or application (SQL only) with their authorization, condition and build option, as the app's role; posted values set only items the user may change, `x01`–`x10` are binds `:X01`–`:X10` and can't replace the built-in binds; at most 1000 rows come back. The download action runs only the download process its own (visible) dynamic action names. The error handling function runs as the app's role in its own transaction with a 2 s limit; its failure keeps the original message. *Stay signed in* needs that application's signed-in session and its CSRF token. |
|
|
79
89
|
| Session state protection | Item values in URLs, and the row keys of interactive grids, carry an HMAC checksum bound to app, page and user. Hidden, display, read-only and unauthorized items cannot be set by a form post. |
|
|
80
90
|
| Request integrity | A CSRF token on every POST (pages, AJAX, login, logout). Buttons are re-validated on submit. |
|
|
81
91
|
| Output | Auto-escaping HTML templates, strict CSP (no inline scripts, no inline styles: the page's one `<style>` carries a per-response nonce), `X-Frame-Options`, `nosniff`, `no-store`. Images may also come from the map tile server (`MAP_TILE_URL`, an http(s) origin only); map data travels as JSON in a `<script type="application/json">` element with `<` escaped. A map area in a report URL (`r<id>_bb`) is parsed into four range-checked numbers before it reaches SQL; anything else is ignored. The browser fetches tiles from that server directly, so it sees users' IP addresses, map areas and the site's origin (sent as `Referer`, which OpenStreetMap requires; never the page's path or query): self-host tiles where that matters. |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 076: lost update detection for forms (APEX: "Prevent Lost Updates" and
|
|
3
|
+
-- "Lock Row" of the Form - Automatic Row Processing (DML) process)
|
|
4
|
+
--
|
|
5
|
+
-- When a form region fetches its row, the page also gets a checksum of the
|
|
6
|
+
-- row's form columns (meta.row_version). An update or delete through the
|
|
7
|
+
-- form_dml process locks the row (FOR UPDATE), computes the checksum again
|
|
8
|
+
-- and refuses the save when it differs: someone else changed the row since
|
|
9
|
+
-- the page was shown.
|
|
10
|
+
--
|
|
11
|
+
-- The checksum is computed under fixed settings (UTC, ISO dates, all float
|
|
12
|
+
-- digits), so it does not depend on the user's time zone or the session's
|
|
13
|
+
-- formats. The function runs with the caller's rights: the application's
|
|
14
|
+
-- role, its grants and its row level security policies.
|
|
15
|
+
-- =====================================================================
|
|
16
|
+
|
|
17
|
+
create function meta.row_version(p_table regclass, p_pk_column text, p_pk text, p_columns text[], p_lock boolean default false)
|
|
18
|
+
returns text
|
|
19
|
+
language plpgsql
|
|
20
|
+
set timezone = 'UTC'
|
|
21
|
+
set datestyle = 'ISO, YMD'
|
|
22
|
+
set intervalstyle = 'iso_8601'
|
|
23
|
+
set extra_float_digits = 3
|
|
24
|
+
set search_path = pg_catalog
|
|
25
|
+
as $$
|
|
26
|
+
declare
|
|
27
|
+
v_cols text;
|
|
28
|
+
v_type text;
|
|
29
|
+
v_out text;
|
|
30
|
+
begin
|
|
31
|
+
-- the key's own type, so the lookup uses the primary key's index
|
|
32
|
+
select format_type(a.atttypid, a.atttypmod) into v_type
|
|
33
|
+
from pg_attribute a where a.attrelid = p_table and a.attname = p_pk_column and a.attnum > 0 and not a.attisdropped;
|
|
34
|
+
if v_type is null then
|
|
35
|
+
raise exception 'column % does not exist in %', p_pk_column, p_table;
|
|
36
|
+
end if;
|
|
37
|
+
-- each column as text in the row, in the given order
|
|
38
|
+
select string_agg(format('%I::text', c), ', ' order by o) into v_cols
|
|
39
|
+
from unnest(p_columns) with ordinality as x(c, o);
|
|
40
|
+
execute format('select md5(row(%s)::text) from %s where %I = $1::%s%s',
|
|
41
|
+
coalesce(v_cols, format('%I::text', p_pk_column)), p_table, p_pk_column, v_type,
|
|
42
|
+
case when p_lock then ' for update' else '' end)
|
|
43
|
+
into v_out using p_pk;
|
|
44
|
+
return v_out;
|
|
45
|
+
end
|
|
46
|
+
$$;
|
|
47
|
+
grant execute on function meta.row_version(regclass, text, text, text[], boolean) to public;
|
|
48
|
+
|
|
49
|
+
comment on function meta.row_version(regclass, text, text, text[], boolean) is
|
|
50
|
+
'A checksum of a row''s columns (lost update detection of forms, 076); NULL when the row is not found or not visible';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 077: warn on unsaved changes (APEX: the page attribute "Warn on Unsaved
|
|
3
|
+
-- Changes" and the item attribute "Warn on Unsaved Changes: Ignore")
|
|
4
|
+
--
|
|
5
|
+
-- With meta.page.warn_unsaved the browser asks before the user leaves the
|
|
6
|
+
-- page (a link, a redirect button, closing or reloading the tab) while an
|
|
7
|
+
-- item of the page form has a changed value. Submitting the page doesn't
|
|
8
|
+
-- ask. An item is left out with {"warn_unsaved": false} in its attributes.
|
|
9
|
+
-- Off for existing pages; the create page wizards switch it on for forms.
|
|
10
|
+
-- =====================================================================
|
|
11
|
+
|
|
12
|
+
alter table meta.page add column warn_unsaved boolean not null default false;
|
|
13
|
+
|
|
14
|
+
-- an export made before 077 has no warn_unsaved: NULL becomes false
|
|
15
|
+
create function meta.page_warn_unsaved_default() returns trigger
|
|
16
|
+
language plpgsql set search_path = pg_catalog as $$
|
|
17
|
+
begin
|
|
18
|
+
new.warn_unsaved := coalesce(new.warn_unsaved, false);
|
|
19
|
+
return new;
|
|
20
|
+
end $$;
|
|
21
|
+
create trigger page_warn_unsaved before insert or update on meta.page
|
|
22
|
+
for each row execute function meta.page_warn_unsaved_default();
|
|
23
|
+
|
|
24
|
+
comment on column meta.page.warn_unsaved is 'Ask before leaving the page while items have unsaved changes (077)';
|
|
25
|
+
|
|
26
|
+
-- The create page wizards switch it on for the form pages they make (APEX's default
|
|
27
|
+
-- for new pages): meta.wizard_form (047) is wrapped.
|
|
28
|
+
alter function meta.wizard_form(meta.app, regclass, int, text, int, boolean, text[]) rename to wizard_form_047;
|
|
29
|
+
create function meta.wizard_form(
|
|
30
|
+
p_app meta.app, p_table regclass, p_page int, p_label text, p_return int, p_modal boolean, p_columns text[] default null
|
|
31
|
+
) returns int
|
|
32
|
+
language plpgsql as $$
|
|
33
|
+
declare
|
|
34
|
+
v_page int := meta.wizard_form_047(p_app, p_table, p_page, p_label, p_return, p_modal, p_columns);
|
|
35
|
+
begin
|
|
36
|
+
update meta.page set warn_unsaved = true where id = v_page;
|
|
37
|
+
return v_page;
|
|
38
|
+
end
|
|
39
|
+
$$;
|
|
40
|
+
-- like the original (047): for the builder only, not for applications
|
|
41
|
+
revoke all on function meta.wizard_form(meta.app, regclass, int, text, int, boolean, text[]) from public;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 078: the URL region (APEX: URL region): another site's page in the
|
|
3
|
+
-- region, in a sandboxed iframe. The source is the URL; its origin is
|
|
4
|
+
-- literal (an item can't choose the host), and the page's
|
|
5
|
+
-- Content-Security-Policy frames exactly the origins of its URL regions
|
|
6
|
+
-- (src/runtime/url-region.ts).
|
|
7
|
+
-- =====================================================================
|
|
8
|
+
|
|
9
|
+
alter table meta.region drop constraint region_type_check;
|
|
10
|
+
alter table meta.region add constraint region_type_check check (type in (
|
|
11
|
+
'report', 'form', 'chart', 'cards', 'static', 'grid', 'calendar', 'dynamic', 'facets', 'tasks', 'workflows', 'map', 'tree',
|
|
12
|
+
'template_component', 'smart_filters', 'display_selector', 'list', 'data_reporter', 'ai_assistant', 'plugin', 'url'));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 079: the developer toolbar (APEX: Runtime Developer Toolbar) and the
|
|
3
|
+
-- session state viewer
|
|
4
|
+
--
|
|
5
|
+
-- A builder session hands the browser a second, random token in the cookie
|
|
6
|
+
-- pgkiln_devbar (path /a, HttpOnly). Pages of an application show the
|
|
7
|
+
-- developer toolbar when that token belongs to a live builder session of a
|
|
8
|
+
-- developer who may open the application (src/runtime/devbar.ts). The token
|
|
9
|
+
-- only shows the toolbar: it is not the builder session's token.
|
|
10
|
+
-- =====================================================================
|
|
11
|
+
|
|
12
|
+
alter table meta.session add column devbar_hash text;
|
|
13
|
+
create unique index session_devbar_hash on meta.session (devbar_hash) where devbar_hash is not null;
|
|
14
|
+
|
|
15
|
+
comment on column meta.session.devbar_hash is 'Builder sessions: sha256 of the developer toolbar token (079)';
|
|
16
|
+
|
|
17
|
+
-- Debug messages of a session the toolbar put in debug (state __DEBUG_LEVEL, set only by
|
|
18
|
+
-- POST /a/<alias>/<page>/devbar/debug for a developer) are stored even when the
|
|
19
|
+
-- application is not in debug. Same as 051 otherwise.
|
|
20
|
+
create or replace function meta.debug_save(p_app_id int, p_page_no int, p_username text, p_session_id uuid, p_method text, p_path text,
|
|
21
|
+
p_status int, p_level int, p_started_at timestamptz, p_elapsed_ms numeric, p_entries jsonb)
|
|
22
|
+
returns bigint
|
|
23
|
+
language plpgsql security definer set search_path = meta, pg_catalog as $$
|
|
24
|
+
declare
|
|
25
|
+
v_id bigint;
|
|
26
|
+
v_level int;
|
|
27
|
+
begin
|
|
28
|
+
-- only for an application that is in debug now, or (079) a session the developer toolbar put in debug
|
|
29
|
+
select debug_level into v_level from meta.app where id = p_app_id;
|
|
30
|
+
if coalesce(v_level, 0) = 0 then
|
|
31
|
+
select (s.state->>'__DEBUG_LEVEL')::int into v_level
|
|
32
|
+
from meta.session s
|
|
33
|
+
where s.id = p_session_id and s.app_id = p_app_id and s.state->>'__DEBUG_LEVEL' in ('1', '2', '4', '6', '9');
|
|
34
|
+
end if;
|
|
35
|
+
if coalesce(v_level, 0) = 0 then
|
|
36
|
+
return null;
|
|
37
|
+
end if;
|
|
38
|
+
insert into meta.debug_view (app_id, page_no, username, session_id, method, path, status, level, started_at, elapsed_ms, entries)
|
|
39
|
+
values (p_app_id, p_page_no, left(p_username, 200), p_session_id, left(p_method, 10), left(p_path, 500), p_status,
|
|
40
|
+
least(greatest(coalesce(p_level, v_level), 1), 9), coalesce(p_started_at, now()), p_elapsed_ms,
|
|
41
|
+
least(coalesce(jsonb_array_length(p_entries), 0), 2000))
|
|
42
|
+
returning id into v_id;
|
|
43
|
+
insert into meta.debug_message (view_id, seq, elapsed_ms, duration_ms, level, component, message)
|
|
44
|
+
select v_id, e.ord::int, coalesce((e.v->>'ms')::numeric(10, 1), 0), (e.v->>'dur')::numeric(10, 1),
|
|
45
|
+
least(greatest(coalesce((e.v->>'level')::int, 4), 1), 9), left(e.v->>'component', 100), left(coalesce(e.v->>'text', ''), 4000)
|
|
46
|
+
from jsonb_array_elements(coalesce(p_entries, '[]')) with ordinality as e(v, ord)
|
|
47
|
+
where e.ord <= 2000;
|
|
48
|
+
if random() < 0.02 then
|
|
49
|
+
perform meta.debug_purge();
|
|
50
|
+
end if;
|
|
51
|
+
return v_id;
|
|
52
|
+
end
|
|
53
|
+
$$;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- 080: search configurations and the search region (APEX 22.2: Search
|
|
3
|
+
-- Configurations in Shared Components, the Search region)
|
|
4
|
+
--
|
|
5
|
+
-- A search configuration says what one source contributes to a search: a
|
|
6
|
+
-- table or view, a SELECT, or a list (Shared Components → Lists); the
|
|
7
|
+
-- columns searched; and which columns make each result's title, subtitle,
|
|
8
|
+
-- description, badge and icon, and where a result links to. A region of
|
|
9
|
+
-- type "search" searches the configurations it names with one search
|
|
10
|
+
-- field (or a page item) and shows the results grouped per configuration
|
|
11
|
+
-- (src/runtime/search.ts). The searches run as the application's role, the
|
|
12
|
+
-- search text is a query parameter.
|
|
13
|
+
--
|
|
14
|
+
-- export_app / import_app are redefined from 069 to carry search_configs.
|
|
15
|
+
-- =====================================================================
|
|
16
|
+
|
|
17
|
+
create table meta.search_config (
|
|
18
|
+
id serial primary key,
|
|
19
|
+
app_id int not null references meta.app (id) on delete cascade,
|
|
20
|
+
name text not null check (name ~ '^[A-Z][A-Z0-9_]{0,59}$'),
|
|
21
|
+
label text not null check (length(label) between 1 and 200),
|
|
22
|
+
seq int not null default 10,
|
|
23
|
+
source_type text not null default 'table' check (source_type in ('table', 'sql', 'list')),
|
|
24
|
+
-- a table or view (schema.name), a SELECT, or the name of a list
|
|
25
|
+
source text not null,
|
|
26
|
+
-- the columns the search text is looked for in (empty: title, subtitle and description)
|
|
27
|
+
searchable_columns text[] not null default '{}',
|
|
28
|
+
title_column text,
|
|
29
|
+
subtitle_column text,
|
|
30
|
+
description_column text,
|
|
31
|
+
badge_column text,
|
|
32
|
+
-- an icon name, or #COLUMN# for a column that holds icon names
|
|
33
|
+
icon text,
|
|
34
|
+
-- where a result links to: {"page": 3, "items": {"P3_ID": "#ID#"}}
|
|
35
|
+
link jsonb,
|
|
36
|
+
max_results int not null default 10 check (max_results between 1 and 100),
|
|
37
|
+
build_option text,
|
|
38
|
+
unique (app_id, name)
|
|
39
|
+
);
|
|
40
|
+
grant select on meta.search_config to pgkiln_runtime;
|
|
41
|
+
|
|
42
|
+
-- an export made before 080 has none; one made with fewer columns gets the defaults
|
|
43
|
+
create function meta.search_config_defaults() returns trigger
|
|
44
|
+
language plpgsql set search_path = pg_catalog as $$
|
|
45
|
+
begin
|
|
46
|
+
new.seq := coalesce(new.seq, 10);
|
|
47
|
+
new.source_type := coalesce(new.source_type, 'table');
|
|
48
|
+
new.searchable_columns := coalesce(new.searchable_columns, '{}');
|
|
49
|
+
new.max_results := coalesce(new.max_results, 10);
|
|
50
|
+
return new;
|
|
51
|
+
end $$;
|
|
52
|
+
create trigger search_config_defaults before insert or update on meta.search_config
|
|
53
|
+
for each row execute function meta.search_config_defaults();
|
|
54
|
+
|
|
55
|
+
comment on table meta.search_config is 'Search configurations (080): what a search region searches, exported with the application';
|
|
56
|
+
|
|
57
|
+
alter table meta.region drop constraint region_type_check;
|
|
58
|
+
alter table meta.region add constraint region_type_check check (type in (
|
|
59
|
+
'report', 'form', 'chart', 'cards', 'static', 'grid', 'calendar', 'dynamic', 'facets', 'tasks', 'workflows', 'map', 'tree',
|
|
60
|
+
'template_component', 'smart_filters', 'display_selector', 'list', 'data_reporter', 'ai_assistant', 'plugin', 'url', 'search'));
|
|
61
|
+
|
|
62
|
+
-- ---------------------------------------------------------------- export / import
|
|
63
|
+
-- Same as 069, plus search_configs.
|
|
64
|
+
create or replace function meta.export_app(p_alias text) returns jsonb
|
|
65
|
+
language sql stable set search_path = meta, pg_catalog as $$
|
|
66
|
+
select jsonb_build_object(
|
|
67
|
+
'format', 'pgkiln/2',
|
|
68
|
+
'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at',
|
|
69
|
+
'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), '[]'),
|
|
70
|
+
'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), '[]'),
|
|
71
|
+
'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), '[]'),
|
|
72
|
+
'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), '[]'),
|
|
73
|
+
'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), '[]'),
|
|
74
|
+
'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), '[]'),
|
|
75
|
+
'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), '[]'),
|
|
76
|
+
'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)
|
|
77
|
+
from meta.report_layout x where x.app_id = a.id), '[]'),
|
|
78
|
+
'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
|
|
79
|
+
from meta.automation x where x.app_id = a.id), '[]'),
|
|
80
|
+
-- (044) the actions of the automations, by automation name
|
|
81
|
+
'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")
|
|
82
|
+
from meta.automation_action x where x.app_id = a.id), '[]'),
|
|
83
|
+
'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), '[]'),
|
|
84
|
+
'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), '[]'),
|
|
85
|
+
'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), '[]'),
|
|
86
|
+
'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), '[]'),
|
|
87
|
+
'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), '[]'),
|
|
88
|
+
'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), '[]'),
|
|
89
|
+
-- (030) secrets never leave the installation: they are entered again after an import
|
|
90
|
+
-- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
|
|
91
|
+
'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)
|
|
92
|
+
from meta.web_credential x where x.app_id = a.id), '[]'),
|
|
93
|
+
-- (050) the synchronisation's state (next and last run) belongs to the installation
|
|
94
|
+
'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)
|
|
95
|
+
from meta.rest_source x where x.app_id = a.id), '[]'),
|
|
96
|
+
-- (041) data load definitions
|
|
97
|
+
'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), '[]'),
|
|
98
|
+
-- (042) lists; entries keep their ids so parents can be remapped on import
|
|
99
|
+
'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), '[]'),
|
|
100
|
+
'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), '[]'),
|
|
101
|
+
-- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
|
|
102
|
+
'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), '[]'),
|
|
103
|
+
-- (069) plug-ins (their files are static files, their templates template components)
|
|
104
|
+
'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), '[]'),
|
|
105
|
+
-- (080) search configurations
|
|
106
|
+
'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), '[]'),
|
|
107
|
+
-- (068) static application files (JavaScript, CSS, images, fonts) as base64
|
|
108
|
+
'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)
|
|
109
|
+
from meta.static_file x where x.app_id = a.id), '[]'),
|
|
110
|
+
-- nav entries and regions keep their ids, so parents and references can be remapped on import
|
|
111
|
+
'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), '[]'),
|
|
112
|
+
'pages', coalesce((
|
|
113
|
+
select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
|
|
114
|
+
'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), '[]'),
|
|
115
|
+
'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), '[]'),
|
|
116
|
+
'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), '[]'),
|
|
117
|
+
'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), '[]'),
|
|
118
|
+
'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), '[]'),
|
|
119
|
+
'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), '[]'),
|
|
120
|
+
'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), '[]'),
|
|
121
|
+
'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), '[]')
|
|
122
|
+
) order by p.page_no)
|
|
123
|
+
from meta.page p where p.app_id = a.id), '[]'))
|
|
124
|
+
from meta.app a
|
|
125
|
+
where a.alias = p_alias
|
|
126
|
+
$$;
|
|
127
|
+
|
|
128
|
+
create or replace function meta.import_app(p_doc jsonb, p_alias text default null) returns int
|
|
129
|
+
language plpgsql set search_path = meta, pg_catalog as $$
|
|
130
|
+
declare
|
|
131
|
+
v_app_id int;
|
|
132
|
+
v_page_id int;
|
|
133
|
+
v_page jsonb;
|
|
134
|
+
v_e jsonb;
|
|
135
|
+
v_rmap jsonb;
|
|
136
|
+
v_nmap jsonb := '{}';
|
|
137
|
+
v_lmap jsonb := '{}';
|
|
138
|
+
v_new_id int;
|
|
139
|
+
begin
|
|
140
|
+
if p_doc->>'format' is distinct from 'pgkiln/2' then
|
|
141
|
+
raise exception 'unsupported export format %', coalesce(p_doc->>'format', '(none)');
|
|
142
|
+
end if;
|
|
143
|
+
|
|
144
|
+
insert into meta.app
|
|
145
|
+
select (jsonb_populate_record(null::meta.app, p_doc->'app' || jsonb_build_object(
|
|
146
|
+
'id', nextval('meta.app_id_seq'),
|
|
147
|
+
'alias', coalesce(p_alias, p_doc->'app'->>'alias'),
|
|
148
|
+
'created_at', now(), 'updated_at', now()))).*
|
|
149
|
+
returning id into v_app_id;
|
|
150
|
+
|
|
151
|
+
insert into meta.authz_scheme
|
|
152
|
+
select (jsonb_populate_record(null::meta.authz_scheme, e || jsonb_build_object('id', nextval('meta.authz_scheme_id_seq'), 'app_id', v_app_id))).*
|
|
153
|
+
from jsonb_array_elements(coalesce(p_doc->'authz_schemes', '[]')) e;
|
|
154
|
+
insert into meta.app_item
|
|
155
|
+
select (jsonb_populate_record(null::meta.app_item, e || jsonb_build_object('id', nextval('meta.app_item_id_seq'), 'app_id', v_app_id))).*
|
|
156
|
+
from jsonb_array_elements(coalesce(p_doc->'app_items', '[]')) e;
|
|
157
|
+
insert into meta.app_process
|
|
158
|
+
select (jsonb_populate_record(null::meta.app_process, e || jsonb_build_object('id', nextval('meta.app_process_id_seq'), 'app_id', v_app_id))).*
|
|
159
|
+
from jsonb_array_elements(coalesce(p_doc->'app_processes', '[]')) e;
|
|
160
|
+
insert into meta.lov
|
|
161
|
+
select (jsonb_populate_record(null::meta.lov, e || jsonb_build_object('id', nextval('meta.lov_id_seq'), 'app_id', v_app_id))).*
|
|
162
|
+
from jsonb_array_elements(coalesce(p_doc->'lovs', '[]')) e;
|
|
163
|
+
insert into meta.app_group_role
|
|
164
|
+
select (jsonb_populate_record(null::meta.app_group_role, e || jsonb_build_object('app_id', v_app_id))).*
|
|
165
|
+
from jsonb_array_elements(coalesce(p_doc->'group_roles', '[]')) e;
|
|
166
|
+
insert into meta.text_message
|
|
167
|
+
select (jsonb_populate_record(null::meta.text_message, e || jsonb_build_object('app_id', v_app_id))).*
|
|
168
|
+
from jsonb_array_elements(coalesce(p_doc->'text_messages', '[]')) e;
|
|
169
|
+
insert into meta.translation
|
|
170
|
+
select (jsonb_populate_record(null::meta.translation, e || jsonb_build_object('app_id', v_app_id))).*
|
|
171
|
+
from jsonb_array_elements(coalesce(p_doc->'translations', '[]')) e;
|
|
172
|
+
insert into meta.report_layout
|
|
173
|
+
select (jsonb_populate_record(null::meta.report_layout, (e - 'logo') || jsonb_build_object(
|
|
174
|
+
'id', nextval('meta.report_layout_id_seq'), 'app_id', v_app_id,
|
|
175
|
+
'logo', null))).*
|
|
176
|
+
from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e;
|
|
177
|
+
update meta.report_layout l
|
|
178
|
+
set logo = decode(e->>'logo', 'base64')
|
|
179
|
+
from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e
|
|
180
|
+
where l.app_id = v_app_id and l.name = e->>'name' and e->>'logo' is not null;
|
|
181
|
+
-- imported automations start switched off: the copy must not run the original's jobs unasked.
|
|
182
|
+
-- (044) a file with actions: the automations' code is ignored; an older file: code becomes the single action
|
|
183
|
+
insert into meta.automation
|
|
184
|
+
select (jsonb_populate_record(null::meta.automation, '{"error_handling": "stop"}'::jsonb
|
|
185
|
+
|| jsonb_strip_nulls(case when p_doc ? 'automation_actions' then e - 'code' else e end)
|
|
186
|
+
|| jsonb_build_object('id', nextval('meta.automation_id_seq'), 'app_id', v_app_id, 'enabled', false))).*
|
|
187
|
+
from jsonb_array_elements(coalesce(p_doc->'automations', '[]')) e;
|
|
188
|
+
insert into meta.automation_action
|
|
189
|
+
select (jsonb_populate_record(null::meta.automation_action, '{"seq": 10}'::jsonb || jsonb_strip_nulls(e)
|
|
190
|
+
|| jsonb_build_object('id', nextval('meta.automation_action_id_seq'), 'app_id', v_app_id))).*
|
|
191
|
+
from jsonb_array_elements(coalesce(p_doc->'automation_actions', '[]')) e;
|
|
192
|
+
insert into meta.document_template
|
|
193
|
+
select (jsonb_populate_record(null::meta.document_template, e || jsonb_build_object('id', nextval('meta.document_template_id_seq'), 'app_id', v_app_id))).*
|
|
194
|
+
from jsonb_array_elements(coalesce(p_doc->'document_templates', '[]')) e;
|
|
195
|
+
insert into meta.task_definition
|
|
196
|
+
select (jsonb_populate_record(null::meta.task_definition, e || jsonb_build_object('id', nextval('meta.task_definition_id_seq'), 'app_id', v_app_id))).*
|
|
197
|
+
from jsonb_array_elements(coalesce(p_doc->'task_definitions', '[]')) e;
|
|
198
|
+
insert into meta.workflow_definition
|
|
199
|
+
select (jsonb_populate_record(null::meta.workflow_definition, e || jsonb_build_object('id', nextval('meta.workflow_definition_id_seq'), 'app_id', v_app_id))).*
|
|
200
|
+
from jsonb_array_elements(coalesce(p_doc->'workflow_definitions', '[]')) e;
|
|
201
|
+
insert into meta.rest_module
|
|
202
|
+
select (jsonb_populate_record(null::meta.rest_module, e || jsonb_build_object('id', nextval('meta.rest_module_id_seq'), 'app_id', v_app_id))).*
|
|
203
|
+
from jsonb_array_elements(coalesce(p_doc->'rest_modules', '[]')) e;
|
|
204
|
+
-- (028) template components: regions and report columns refer to them by static id
|
|
205
|
+
insert into meta.template_component
|
|
206
|
+
select (jsonb_populate_record(null::meta.template_component, e || jsonb_build_object('id', nextval('meta.template_component_id_seq'), 'app_id', v_app_id))).*
|
|
207
|
+
from jsonb_array_elements(coalesce(p_doc->'template_components', '[]')) e;
|
|
208
|
+
-- (029) build options (components name them by name)
|
|
209
|
+
insert into meta.build_option
|
|
210
|
+
select (jsonb_populate_record(null::meta.build_option, e || jsonb_build_object('id', nextval('meta.build_option_id_seq'), 'app_id', v_app_id))).*
|
|
211
|
+
from jsonb_array_elements(coalesce(p_doc->'build_options', '[]')) e;
|
|
212
|
+
-- (030) web credentials arrive without a secret, whatever the document holds
|
|
213
|
+
-- (050) nor a password or refresh token
|
|
214
|
+
insert into meta.web_credential
|
|
215
|
+
select (jsonb_populate_record(null::meta.web_credential, '{"type": "basic", "valid_for": [], "grant_type": "client_credentials"}'::jsonb
|
|
216
|
+
|| jsonb_strip_nulls(e - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at')
|
|
217
|
+
|| jsonb_build_object('id', nextval('meta.web_credential_id_seq'), 'app_id', v_app_id))).*
|
|
218
|
+
from jsonb_array_elements(coalesce(p_doc->'web_credentials', '[]')) e;
|
|
219
|
+
-- (050) write-back operations and synchronisation; an imported synchronisation starts switched off
|
|
220
|
+
insert into meta.rest_source
|
|
221
|
+
select (jsonb_populate_record(null::meta.rest_source, '{"method": "GET", "headers": {}, "params": [], "columns": [], "cache_seconds": 0, "timeout_s": 10, "max_rows": 1000,
|
|
222
|
+
"operations": {}, "key_columns": [], "sync_mode": "merge", "sync_delete": false, "sync_time_zone": "UTC"}'::jsonb
|
|
223
|
+
|| jsonb_strip_nulls(e - 'sync_next_at' - 'sync_last_at' - 'sync_last_status')
|
|
224
|
+
|| jsonb_build_object('id', nextval('meta.rest_source_id_seq'), 'app_id', v_app_id, 'sync_enabled', false))).*
|
|
225
|
+
from jsonb_array_elements(coalesce(p_doc->'rest_sources', '[]')) e;
|
|
226
|
+
|
|
227
|
+
-- (041) data load definitions (the data_load process names them)
|
|
228
|
+
insert into meta.data_load_def
|
|
229
|
+
select (jsonb_populate_record(null::meta.data_load_def, '{"format": "auto", "headers": true, "mode": "append", "skip_errors": false, "columns": []}'::jsonb
|
|
230
|
+
|| jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.data_load_def_id_seq'), 'app_id', v_app_id))).*
|
|
231
|
+
from jsonb_array_elements(coalesce(p_doc->'data_load_definitions', '[]')) e;
|
|
232
|
+
|
|
233
|
+
-- (042) lists and their entries, supporting scripts. Entries are inserted
|
|
234
|
+
-- without a parent first and the parents set afterwards, so the order of
|
|
235
|
+
-- the entries in the document doesn't matter (a moved entry may have a
|
|
236
|
+
-- higher id than its children).
|
|
237
|
+
insert into meta.list
|
|
238
|
+
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))).*
|
|
239
|
+
from jsonb_array_elements(coalesce(p_doc->'lists', '[]')) e;
|
|
240
|
+
for v_e in select * from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) loop
|
|
241
|
+
insert into meta.list_entry
|
|
242
|
+
select (jsonb_populate_record(null::meta.list_entry, '{"target_items": {}}'::jsonb || jsonb_strip_nulls(v_e) || jsonb_build_object(
|
|
243
|
+
'id', nextval('meta.list_entry_id_seq'), 'app_id', v_app_id, 'parent_id', null))).*
|
|
244
|
+
returning id into v_new_id;
|
|
245
|
+
if v_e->>'id' is not null then
|
|
246
|
+
v_lmap := v_lmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
247
|
+
end if;
|
|
248
|
+
end loop;
|
|
249
|
+
update meta.list_entry x
|
|
250
|
+
set parent_id = (v_lmap->>(e->>'parent_id'))::int
|
|
251
|
+
from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) e
|
|
252
|
+
where e->>'parent_id' is not null and v_lmap ? (e->>'parent_id') and v_lmap ? (e->>'id')
|
|
253
|
+
and x.id = (v_lmap->>(e->>'id'))::int;
|
|
254
|
+
insert into meta.supporting_script
|
|
255
|
+
select (jsonb_populate_record(null::meta.supporting_script, '{"kind": "install", "seq": 10, "script": ""}'::jsonb || jsonb_strip_nulls(e)
|
|
256
|
+
|| jsonb_build_object('id', nextval('meta.supporting_script_id_seq'), 'app_id', v_app_id))).*
|
|
257
|
+
from jsonb_array_elements(coalesce(p_doc->'supporting_scripts', '[]')) e;
|
|
258
|
+
|
|
259
|
+
-- (069) plug-ins
|
|
260
|
+
insert into meta.plugin
|
|
261
|
+
select (jsonb_populate_record(null::meta.plugin, e || jsonb_build_object('id', nextval('meta.plugin_id_seq'), 'app_id', v_app_id))).*
|
|
262
|
+
from jsonb_array_elements(coalesce(p_doc->'plugins', '[]')) e;
|
|
263
|
+
|
|
264
|
+
-- (080) search configurations (search regions name them)
|
|
265
|
+
insert into meta.search_config
|
|
266
|
+
select (jsonb_populate_record(null::meta.search_config, '{"seq": 10, "source_type": "table", "searchable_columns": [], "max_results": 10}'::jsonb
|
|
267
|
+
|| jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.search_config_id_seq'), 'app_id', v_app_id))).*
|
|
268
|
+
from jsonb_array_elements(coalesce(p_doc->'search_configs', '[]')) e;
|
|
269
|
+
|
|
270
|
+
-- (068) static application files
|
|
271
|
+
insert into meta.static_file (app_id, name, mime, content, updated_at)
|
|
272
|
+
select v_app_id, e->>'name', e->>'mime', decode(coalesce(e->>'content', ''), 'base64'), now()
|
|
273
|
+
from jsonb_array_elements(coalesce(p_doc->'static_files', '[]')) e;
|
|
274
|
+
|
|
275
|
+
for v_e in select * from jsonb_array_elements(coalesce(p_doc->'nav', '[]')) loop
|
|
276
|
+
insert into meta.nav_entry
|
|
277
|
+
select (jsonb_populate_record(null::meta.nav_entry, v_e || jsonb_build_object(
|
|
278
|
+
'id', nextval('meta.nav_entry_id_seq'), 'app_id', v_app_id,
|
|
279
|
+
'parent_id', v_nmap->>(v_e->>'parent_id')))).*
|
|
280
|
+
returning id into v_new_id;
|
|
281
|
+
v_nmap := v_nmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
282
|
+
end loop;
|
|
283
|
+
|
|
284
|
+
for v_page in select * from jsonb_array_elements(coalesce(p_doc->'pages', '[]')) loop
|
|
285
|
+
insert into meta.page
|
|
286
|
+
select (jsonb_populate_record(null::meta.page, v_page || jsonb_build_object('id', nextval('meta.page_id_seq'), 'app_id', v_app_id))).*
|
|
287
|
+
returning id into v_page_id;
|
|
288
|
+
|
|
289
|
+
v_rmap := '{}';
|
|
290
|
+
for v_e in select * from jsonb_array_elements(coalesce(v_page->'regions', '[]')) loop
|
|
291
|
+
insert into meta.region
|
|
292
|
+
select (jsonb_populate_record(null::meta.region, v_e || jsonb_build_object('id', nextval('meta.region_id_seq'), 'page_id', v_page_id))).*
|
|
293
|
+
returning id into v_new_id;
|
|
294
|
+
v_rmap := v_rmap || jsonb_build_object(v_e->>'id', v_new_id);
|
|
295
|
+
end loop;
|
|
296
|
+
-- facet and map regions point at their report region by id
|
|
297
|
+
update meta.region r
|
|
298
|
+
set config = jsonb_set(r.config, '{report}', to_jsonb((v_rmap->>(r.config->>'report'))::int))
|
|
299
|
+
where r.page_id = v_page_id and r.type in ('facets', 'smart_filters', 'map') and v_rmap ? (r.config->>'report');
|
|
300
|
+
|
|
301
|
+
insert into meta.item
|
|
302
|
+
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')))).*
|
|
303
|
+
from jsonb_array_elements(coalesce(v_page->'items', '[]')) e;
|
|
304
|
+
insert into meta.button
|
|
305
|
+
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')))).*
|
|
306
|
+
from jsonb_array_elements(coalesce(v_page->'buttons', '[]')) e;
|
|
307
|
+
insert into meta.dynamic_action
|
|
308
|
+
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')))).*
|
|
309
|
+
from jsonb_array_elements(coalesce(v_page->'dynamic_actions', '[]')) e;
|
|
310
|
+
insert into meta.validation
|
|
311
|
+
select (jsonb_populate_record(null::meta.validation, e || jsonb_build_object('id', nextval('meta.validation_id_seq'), 'page_id', v_page_id))).*
|
|
312
|
+
from jsonb_array_elements(coalesce(v_page->'validations', '[]')) e;
|
|
313
|
+
insert into meta.process
|
|
314
|
+
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')))).*
|
|
315
|
+
from jsonb_array_elements(coalesce(v_page->'processes', '[]')) e;
|
|
316
|
+
insert into meta.computation
|
|
317
|
+
select (jsonb_populate_record(null::meta.computation, e || jsonb_build_object('id', nextval('meta.computation_id_seq'), 'page_id', v_page_id))).*
|
|
318
|
+
from jsonb_array_elements(coalesce(v_page->'computations', '[]')) e;
|
|
319
|
+
insert into meta.branch
|
|
320
|
+
select (jsonb_populate_record(null::meta.branch, e || jsonb_build_object('id', nextval('meta.branch_id_seq'), 'page_id', v_page_id))).*
|
|
321
|
+
from jsonb_array_elements(coalesce(v_page->'branches', '[]')) e;
|
|
322
|
+
end loop;
|
|
323
|
+
|
|
324
|
+
return v_app_id;
|
|
325
|
+
end
|
|
326
|
+
$$;
|