pgkiln 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/140_item_region_types.sql +27 -0
- package/db/migrations/141_workspace_resources.sql +115 -0
- package/docs/apex-feature-parity.md +91 -12
- package/docs/guide/01-installation.md +1 -0
- package/docs/guide/03-builder.md +47 -4
- package/docs/guide/04-pages-and-regions.md +41 -1
- package/docs/guide/05-items.md +43 -3
- package/docs/guide/11-from-apex.md +3 -0
- package/docs/guide/12-development.md +25 -3
- package/docs/guide/16-files.md +29 -0
- package/docs/guide/19-rest-data-sources.md +41 -3
- package/examples/hr/hr_75_item_region_types.sql +81 -0
- package/examples/hr/hr_76_remote_servers.sql +48 -0
- package/package.json +1 -1
- package/public/app.css +48 -0
- package/public/app.js +377 -1
- package/src/ai/service.ts +10 -1
- package/src/app.ts +2 -0
- package/src/builder/advisor.ts +18 -2
- package/src/builder/ai.ts +29 -3
- package/src/builder/arrange.ts +7 -0
- package/src/builder/components.ts +5 -5
- package/src/builder/designer.ts +9 -3
- package/src/builder/home.ts +3 -0
- package/src/builder/i18n/de.ts +6 -0
- package/src/builder/i18n/en.ts +6 -0
- package/src/builder/i18n/es.ts +6 -0
- package/src/builder/i18n/fr.ts +6 -0
- package/src/builder/i18n/it.ts +6 -0
- package/src/builder/i18n/ja.ts +6 -0
- package/src/builder/i18n/ko.ts +6 -0
- package/src/builder/i18n/nl.ts +6 -0
- package/src/builder/i18n/pt.ts +6 -0
- package/src/builder/i18n/zh-cn.ts +6 -0
- package/src/builder/i18n/zh-tw.ts +6 -0
- package/src/builder/network-access.ts +33 -0
- package/src/builder/region-help.ts +8 -2
- package/src/builder/rest-catalogs.ts +22 -14
- package/src/builder/routes.ts +5 -1
- package/src/builder/static-files.ts +14 -4
- package/src/builder/websources.ts +12 -6
- package/src/builder/workflows.ts +2 -1
- package/src/builder/workspace-resources.ts +528 -0
- package/src/i18n/ar.ts +13 -0
- package/src/i18n/cs.ts +13 -0
- package/src/i18n/da.ts +13 -0
- package/src/i18n/de.ts +13 -0
- package/src/i18n/el.ts +13 -0
- package/src/i18n/es.ts +13 -0
- package/src/i18n/fi.ts +13 -0
- package/src/i18n/fr.ts +13 -0
- package/src/i18n/he.ts +13 -0
- package/src/i18n/it.ts +13 -0
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/nb.ts +13 -0
- package/src/i18n/pl.ts +13 -0
- package/src/i18n/pt.ts +13 -0
- package/src/i18n/ru.ts +13 -0
- package/src/i18n/sv.ts +13 -0
- package/src/i18n/tr.ts +13 -0
- package/src/i18n/uk.ts +13 -0
- package/src/i18n/zh.ts +13 -0
- package/src/i18n.ts +26 -0
- package/src/metadata.ts +22 -5
- package/src/remote-servers.ts +133 -0
- package/src/restcatalog.ts +21 -10
- package/src/restsync.ts +1 -1
- package/src/runtime/context.ts +2 -0
- package/src/runtime/items.ts +32 -3
- package/src/runtime/page-regions.ts +50 -0
- package/src/runtime/region-cache.ts +1 -1
- package/src/runtime/regions.ts +11 -0
- package/src/runtime/render.ts +3 -17
- package/src/runtime/report.ts +55 -3
- package/src/runtime/static-files.ts +46 -17
- package/src/runtime/template-components.ts +2 -1
- package/src/websources.ts +43 -18
- package/src/workspace-export.ts +80 -1
|
@@ -5,6 +5,7 @@ import { icon } from '../icons.ts';
|
|
|
5
5
|
import { readMultipart } from '../runtime/files.ts';
|
|
6
6
|
import { includable, STATIC_MAX, STATIC_TYPES, staticType } from '../runtime/static-files.ts';
|
|
7
7
|
import type { Session } from '../session.ts';
|
|
8
|
+
import { WORKSPACE_FILES } from '../metadata.ts';
|
|
8
9
|
import { appOr404 } from './forms.ts';
|
|
9
10
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, send, shell, type Body, type Req } from './ui.ts';
|
|
10
11
|
|
|
@@ -17,9 +18,15 @@ import { appHeader, back, BASE, csrf, developer, flash, input, region, send, she
|
|
|
17
18
|
/** Files a list may name (every page, or one page). */
|
|
18
19
|
const MAX_INCLUDES = 20;
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* "a.js, b.css" → the valid, includable, distinct names, in order. (141) "#WORKSPACE_FILES#theme.css"
|
|
23
|
+
* is a static workspace file; "#APP_FILES#a.js" is the same as "a.js".
|
|
24
|
+
*/
|
|
21
25
|
export function parseIncludes(text: string | undefined) {
|
|
22
|
-
const names = (text ?? '').split(/[\s,]+/).map((n) => n.trim()).filter((n) =>
|
|
26
|
+
const names = (text ?? '').split(/[\s,]+/).map((n) => n.trim().replace(/^#APP_FILES#/, '')).filter((n) => {
|
|
27
|
+
const file = n.startsWith(WORKSPACE_FILES) ? n.slice(WORKSPACE_FILES.length) : n;
|
|
28
|
+
return file && staticType(file) && includable(file);
|
|
29
|
+
});
|
|
23
30
|
return [...new Set(names)].slice(0, MAX_INCLUDES);
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -84,7 +91,7 @@ async function page(s: Session, a: any, query: Record<string, string | undefined
|
|
|
84
91
|
<small class="help">A file with the same name is replaced.</small>
|
|
85
92
|
</form>`)}
|
|
86
93
|
${region('Every page loads', html`<form method="post" action="${base}/includes">${csrf(s)}
|
|
87
|
-
<div class="form-grid">${input('includes', 'Scripts and stylesheets, in order', (a.static_includes ?? []).join(', '), { placeholder: 'e.g. library.js, app.js, app-styles.css', help: `Up to ${MAX_INCLUDES} .js and .css files, loaded in this order after pgkiln's own (scripts deferred). A page adds its own files under Page → Appearance.` })}</div>
|
|
94
|
+
<div class="form-grid">${input('includes', 'Scripts and stylesheets, in order', (a.static_includes ?? []).join(', '), { placeholder: 'e.g. library.js, app.js, app-styles.css', help: `Up to ${MAX_INCLUDES} .js and .css files, loaded in this order after pgkiln's own (scripts deferred). #WORKSPACE_FILES#name is a static workspace file (Workspace utilities). A page adds its own files under Page → Appearance.` })}</div>
|
|
88
95
|
<div class="buttons"><button class="btn btn-hot">Save</button></div>
|
|
89
96
|
</form>`)}`;
|
|
90
97
|
}
|
|
@@ -191,7 +198,10 @@ export async function staticFileBuilderRoutes(app: FastifyInstance) {
|
|
|
191
198
|
if (!a) return reply.code(404).send('Not found');
|
|
192
199
|
const names = parseIncludes(req.body?.includes);
|
|
193
200
|
await owner.query('update meta.app set static_includes = $2, updated_at = now() where id = $1', [a.id, names]);
|
|
194
|
-
const missing = (await owner.query<{ n: string }>(
|
|
201
|
+
const missing = (await owner.query<{ n: string }>(
|
|
202
|
+
`select n from unnest($2::text[]) n
|
|
203
|
+
where not exists (select 1 from meta.static_file f where f.app_id = $1 and f.name = n)
|
|
204
|
+
and not exists (select 1 from meta.workspace_file w where w.workspace_id = meta.app_workspace($1) and $3 || w.name = n)`, [a.id, names, WORKSPACE_FILES])).rows.map((r) => r.n);
|
|
195
205
|
flash(s, missing.length ? `Saved. Not uploaded yet (left out until they are): ${missing.join(', ')}.` : 'Saved.');
|
|
196
206
|
return back(reply, s, `${BASE}/apps/${a.id}/static-files`);
|
|
197
207
|
});
|
|
@@ -9,6 +9,7 @@ import { runSync, syncLog, syncProblems } from '../restsync.ts';
|
|
|
9
9
|
import { buildRequest, call, clearResponseCache, clearTokens, credentialProblems, guessColumns, loadCredential, parseJson, rowsSql, select, sourceProblems, toRows, withRest, type RestColumn, type RestSource } from '../websources.ts';
|
|
10
10
|
import type { ComponentSpec } from './components.ts';
|
|
11
11
|
import { back, BASE, csrf, developer, flash, type Req } from './ui.ts';
|
|
12
|
+
import { resolveSource } from '../remote-servers.ts';
|
|
12
13
|
|
|
13
14
|
// Shared Components → Web credentials and REST data sources: their property
|
|
14
15
|
// specs, the secrets' status (never a secret), "Test" for a source (its
|
|
@@ -75,7 +76,7 @@ export const REST_SOURCE_SPEC: ComponentSpec = {
|
|
|
75
76
|
if (v.sync_time_zone === null) v.sync_time_zone = 'UTC';
|
|
76
77
|
if (v.sync_mode === null) v.sync_mode = 'merge';
|
|
77
78
|
const problems = [
|
|
78
|
-
...sourceProblems({ url: v.url, params: json(v.params), columns: json(v.columns), headers: json(v.headers), row_selector: v.row_selector, key_columns: v.key_columns, operations: json(v.operations) }),
|
|
79
|
+
...sourceProblems({ url: v.url, server: v.server, params: json(v.params), columns: json(v.columns), headers: json(v.headers), row_selector: v.row_selector, key_columns: v.key_columns, operations: json(v.operations) }),
|
|
79
80
|
...syncProblems(v),
|
|
80
81
|
];
|
|
81
82
|
return problems.length ? problems.join(' ') : null;
|
|
@@ -88,10 +89,12 @@ export const REST_SOURCE_SPEC: ComponentSpec = {
|
|
|
88
89
|
fields: [
|
|
89
90
|
{ name: 'name', label: 'Name', kind: 'upper', group: 'Identification', help: 'Regions and lists of values use it by this name, e.g. COUNTRIES.' },
|
|
90
91
|
{ name: 'description', label: 'Description', kind: 'text', wide: true, group: 'Identification' },
|
|
92
|
+
{ name: 'server', label: 'Remote server', kind: 'upper', group: 'Request',
|
|
93
|
+
help: 'Optional: the static id of a remote server of the workspace (Workspace utilities → Remote servers), e.g. CRM. The URL is then the path after the server\'s base URL, and changing the server moves every source on it.' },
|
|
91
94
|
{ name: 'url', label: 'URL', kind: 'text', wide: true, group: 'Request',
|
|
92
|
-
help: 'e.g. https://api.example.com/v1/cities/{city}/weather — {name} is a path parameter. The host must be on the server\'s allow-list (PGKILN_REST_ALLOWED_HOSTS).' },
|
|
95
|
+
help: 'e.g. https://api.example.com/v1/cities/{city}/weather — {name} is a path parameter. With a remote server: the path, e.g. /v1/cities/{city}/weather. The host must be on the server\'s allow-list (PGKILN_REST_ALLOWED_HOSTS).' },
|
|
93
96
|
{ name: 'method', label: 'Method', kind: 'select', options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], group: 'Request' },
|
|
94
|
-
{ name: 'credential', label: 'Web credential', kind: 'upper', group: 'Request', help: 'The name of a web credential, or empty.' },
|
|
97
|
+
{ name: 'credential', label: 'Web credential', kind: 'upper', group: 'Request', help: 'The name of a web credential (the application\'s, else one of the workspace\'s), or empty: on a remote server, the server\'s default credential.' },
|
|
95
98
|
{ name: 'params', label: 'Parameters (JSON)', kind: 'json', wide: true, group: 'Request',
|
|
96
99
|
help: '[{"name": "city", "in": "path", "default": "&P1_CITY.", "required": true}, {"name": "units", "in": "query", "default": "metric"}] · in: path, query, header, body. Defaults may use &ITEM. substitutions; regions override them with {"rest_params": {…}}.' },
|
|
97
100
|
{ name: 'headers', label: 'Headers (JSON)', kind: 'json', group: 'Request', help: '{"Accept-Language": "en"} — not for secrets: use a web credential.' },
|
|
@@ -267,11 +270,14 @@ export async function webSourceRoutes(app: FastifyInstance) {
|
|
|
267
270
|
app.post(`${BASE}/apps/:id(^\\d+$)/rest-sources/:sid(^\\d+$)/test`, async (req: Req, reply) => {
|
|
268
271
|
const s = await developer(req, reply);
|
|
269
272
|
if (!s) return;
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
272
|
-
const result: TestResult = { id:
|
|
273
|
+
const stored = await owner.one<RestSource>('select * from meta.rest_source where id = $1 and app_id = $2', [req.params.sid, req.params.id]);
|
|
274
|
+
if (!stored) return reply.code(404).send('Not found');
|
|
275
|
+
const result: TestResult = { id: stored.id, ok: false, message: '' };
|
|
273
276
|
const started = Date.now();
|
|
277
|
+
let src = stored;
|
|
274
278
|
try {
|
|
279
|
+
// (141) on a remote server: its URL now
|
|
280
|
+
src = await resolveSource(stored);
|
|
275
281
|
const values: Record<string, string> = {};
|
|
276
282
|
for (const p of src.params) values[p.name] = String(req.body?.[`p_${p.name}`] ?? '');
|
|
277
283
|
const r = buildRequest(src, values);
|
package/src/builder/workflows.ts
CHANGED
|
@@ -73,7 +73,8 @@ export async function workflowExtras(appId: number, row: Definition, s: Session,
|
|
|
73
73
|
const shown = dev ? (row.dev_steps ?? []) : row.steps;
|
|
74
74
|
const refs = {
|
|
75
75
|
sources: new Map((await owner.query('select name, params, columns from meta.rest_source where app_id = $1', [appId])).rows.map((r) => [r.name as string, r])),
|
|
76
|
-
|
|
76
|
+
// (141) the application's credentials and its workspace's
|
|
77
|
+
credentials: new Set((await owner.query('select name from meta.web_credential where app_id = $1 union select name from meta.workspace_credential where workspace_id = meta.app_workspace($1)', [appId])).rows.map((r) => r.name as string)),
|
|
77
78
|
startVars: [...String((row as { title?: string }).title ?? '').matchAll(/&([A-Za-z][A-Za-z0-9_]*)\./g)].map((m) => m[1]),
|
|
78
79
|
};
|
|
79
80
|
const invokeRefs = invokeStepReferences(shown, refs);
|