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.
Files changed (79) hide show
  1. package/db/migrations/140_item_region_types.sql +27 -0
  2. package/db/migrations/141_workspace_resources.sql +115 -0
  3. package/docs/apex-feature-parity.md +91 -12
  4. package/docs/guide/01-installation.md +1 -0
  5. package/docs/guide/03-builder.md +47 -4
  6. package/docs/guide/04-pages-and-regions.md +41 -1
  7. package/docs/guide/05-items.md +43 -3
  8. package/docs/guide/11-from-apex.md +3 -0
  9. package/docs/guide/12-development.md +25 -3
  10. package/docs/guide/16-files.md +29 -0
  11. package/docs/guide/19-rest-data-sources.md +41 -3
  12. package/examples/hr/hr_75_item_region_types.sql +81 -0
  13. package/examples/hr/hr_76_remote_servers.sql +48 -0
  14. package/package.json +1 -1
  15. package/public/app.css +48 -0
  16. package/public/app.js +377 -1
  17. package/src/ai/service.ts +10 -1
  18. package/src/app.ts +2 -0
  19. package/src/builder/advisor.ts +18 -2
  20. package/src/builder/ai.ts +29 -3
  21. package/src/builder/arrange.ts +7 -0
  22. package/src/builder/components.ts +5 -5
  23. package/src/builder/designer.ts +9 -3
  24. package/src/builder/home.ts +3 -0
  25. package/src/builder/i18n/de.ts +6 -0
  26. package/src/builder/i18n/en.ts +6 -0
  27. package/src/builder/i18n/es.ts +6 -0
  28. package/src/builder/i18n/fr.ts +6 -0
  29. package/src/builder/i18n/it.ts +6 -0
  30. package/src/builder/i18n/ja.ts +6 -0
  31. package/src/builder/i18n/ko.ts +6 -0
  32. package/src/builder/i18n/nl.ts +6 -0
  33. package/src/builder/i18n/pt.ts +6 -0
  34. package/src/builder/i18n/zh-cn.ts +6 -0
  35. package/src/builder/i18n/zh-tw.ts +6 -0
  36. package/src/builder/network-access.ts +33 -0
  37. package/src/builder/region-help.ts +8 -2
  38. package/src/builder/rest-catalogs.ts +22 -14
  39. package/src/builder/routes.ts +5 -1
  40. package/src/builder/static-files.ts +14 -4
  41. package/src/builder/websources.ts +12 -6
  42. package/src/builder/workflows.ts +2 -1
  43. package/src/builder/workspace-resources.ts +528 -0
  44. package/src/i18n/ar.ts +13 -0
  45. package/src/i18n/cs.ts +13 -0
  46. package/src/i18n/da.ts +13 -0
  47. package/src/i18n/de.ts +13 -0
  48. package/src/i18n/el.ts +13 -0
  49. package/src/i18n/es.ts +13 -0
  50. package/src/i18n/fi.ts +13 -0
  51. package/src/i18n/fr.ts +13 -0
  52. package/src/i18n/he.ts +13 -0
  53. package/src/i18n/it.ts +13 -0
  54. package/src/i18n/ja.ts +13 -0
  55. package/src/i18n/ko.ts +13 -0
  56. package/src/i18n/nb.ts +13 -0
  57. package/src/i18n/pl.ts +13 -0
  58. package/src/i18n/pt.ts +13 -0
  59. package/src/i18n/ru.ts +13 -0
  60. package/src/i18n/sv.ts +13 -0
  61. package/src/i18n/tr.ts +13 -0
  62. package/src/i18n/uk.ts +13 -0
  63. package/src/i18n/zh.ts +13 -0
  64. package/src/i18n.ts +26 -0
  65. package/src/metadata.ts +22 -5
  66. package/src/remote-servers.ts +133 -0
  67. package/src/restcatalog.ts +21 -10
  68. package/src/restsync.ts +1 -1
  69. package/src/runtime/context.ts +2 -0
  70. package/src/runtime/items.ts +32 -3
  71. package/src/runtime/page-regions.ts +50 -0
  72. package/src/runtime/region-cache.ts +1 -1
  73. package/src/runtime/regions.ts +11 -0
  74. package/src/runtime/render.ts +3 -17
  75. package/src/runtime/report.ts +55 -3
  76. package/src/runtime/static-files.ts +46 -17
  77. package/src/runtime/template-components.ts +2 -1
  78. package/src/websources.ts +43 -18
  79. 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
- /** "a.js, b.css" → the valid, includable, distinct names, in order. */
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) => n && staticType(n) && includable(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 }>('select n from unnest($2::text[]) n where not exists (select 1 from meta.static_file f where f.app_id = $1 and f.name = n)', [a.id, names])).rows.map((r) => r.n);
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 src = await owner.one<RestSource>('select * from meta.rest_source where id = $1 and app_id = $2', [req.params.sid, req.params.id]);
271
- if (!src) return reply.code(404).send('Not found');
272
- const result: TestResult = { id: src.id, ok: false, message: '' };
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);
@@ -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
- credentials: new Set((await owner.query('select name from meta.web_credential where app_id = $1', [appId])).rows.map((r) => r.name as string)),
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);