pgkiln 0.53.0 → 0.55.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 (83) hide show
  1. package/SECURITY.md +2 -0
  2. package/db/migrations/127_builder_language.sql +16 -0
  3. package/docs/apex-feature-parity.md +7 -7
  4. package/docs/guide/03-builder.md +62 -8
  5. package/docs/guide/12-development.md +50 -3
  6. package/examples/gallery/README.md +37 -6
  7. package/examples/gallery/gallery.json +23 -1
  8. package/examples/gallery/samples/sample-calendar.sql +172 -0
  9. package/examples/gallery/samples/sample-cards.sql +157 -0
  10. package/examples/gallery/samples/sample-charts.sql +265 -0
  11. package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
  12. package/examples/gallery/samples/sample-grids.sql +151 -0
  13. package/examples/gallery/samples/sample-reporting.sql +151 -0
  14. package/examples/gallery/samples/ux-patterns.sql +295 -0
  15. package/examples/gallery/utilities/announcements.json +24 -0
  16. package/examples/gallery/utilities/lookup-lists.json +28 -0
  17. package/examples/gallery/utilities/release-notes.json +41 -0
  18. package/package.json +1 -1
  19. package/public/app.css +2 -0
  20. package/public/builder.css +39 -1
  21. package/public/worksheet.css +3 -2
  22. package/src/builder/advisor.ts +1 -1
  23. package/src/builder/ai-agents.ts +5 -5
  24. package/src/builder/ai-builder.ts +3 -3
  25. package/src/builder/api.ts +1 -1
  26. package/src/builder/autorest.ts +2 -2
  27. package/src/builder/availability.ts +1 -1
  28. package/src/builder/backups.ts +1 -1
  29. package/src/builder/changelog.ts +1 -1
  30. package/src/builder/data-modeler.ts +4 -4
  31. package/src/builder/data-security.ts +1 -1
  32. package/src/builder/dataload.ts +1 -1
  33. package/src/builder/datapump.ts +1 -1
  34. package/src/builder/ddl-tools.ts +2 -2
  35. package/src/builder/designer.ts +134 -124
  36. package/src/builder/diagnostics.ts +2 -2
  37. package/src/builder/errors.ts +10 -12
  38. package/src/builder/feedback.ts +1 -1
  39. package/src/builder/gallery.ts +94 -47
  40. package/src/builder/globalization.ts +1 -1
  41. package/src/builder/home.ts +59 -57
  42. package/src/builder/i18n/de.ts +675 -0
  43. package/src/builder/i18n/en.ts +678 -0
  44. package/src/builder/i18n/es.ts +675 -0
  45. package/src/builder/i18n/fr.ts +675 -0
  46. package/src/builder/i18n/nl.ts +675 -0
  47. package/src/builder/i18n/pt.ts +675 -0
  48. package/src/builder/i18n.ts +116 -0
  49. package/src/builder/language.ts +74 -0
  50. package/src/builder/launchpad.ts +13 -8
  51. package/src/builder/object-browser.ts +1 -1
  52. package/src/builder/plugins.ts +1 -1
  53. package/src/builder/querybuilder.ts +1 -1
  54. package/src/builder/quicksql.ts +1 -1
  55. package/src/builder/rest-modules.ts +2 -2
  56. package/src/builder/routes.ts +88 -66
  57. package/src/builder/sampledata.ts +1 -1
  58. package/src/builder/scheduling.ts +1 -1
  59. package/src/builder/scripts.ts +1 -1
  60. package/src/builder/search.ts +1 -1
  61. package/src/builder/shared.ts +5 -5
  62. package/src/builder/sql-charts.ts +2 -2
  63. package/src/builder/sql-monitor.ts +2 -2
  64. package/src/builder/sql-tools.ts +6 -0
  65. package/src/builder/sql-utilities.ts +3 -3
  66. package/src/builder/sql.ts +24 -22
  67. package/src/builder/static-files.ts +1 -1
  68. package/src/builder/subscriptions.ts +1 -1
  69. package/src/builder/supporting.ts +1 -1
  70. package/src/builder/table-editor.ts +1 -1
  71. package/src/builder/team.ts +7 -5
  72. package/src/builder/themeroller.ts +1 -1
  73. package/src/builder/top-sql.ts +1 -1
  74. package/src/builder/ui.ts +87 -77
  75. package/src/builder/unload.ts +1 -1
  76. package/src/builder/wizards.ts +1 -1
  77. package/src/builder/workflow-migrate.ts +1 -1
  78. package/src/builder/workingcopies.ts +2 -2
  79. package/src/builder/worksheet.ts +1 -1
  80. package/src/builder/workspace-home.ts +62 -59
  81. package/src/builder/workspaces.ts +2 -1
  82. package/src/runtime/charts.ts +4 -1
  83. package/src/runtime/regions.ts +8 -5
@@ -142,7 +142,7 @@ const SQL_SCHEMA = {
142
142
 
143
143
  async function workshopPage(s: Session, reply: FastifyReply, out: { question?: string; schema?: string; sql?: string; error?: string; result?: Raw }) {
144
144
  const list = await schemas(s);
145
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai')}
145
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai', s)}
146
146
  ${await serviceBox(s)}
147
147
  <p><a href="${BASE}/sql/ai/describe">Describe tables for AI ▸</a> <span class="muted">(better answers: say what tables and columns mean)</span></p>
148
148
  ${out.result ?? ''}
@@ -178,7 +178,7 @@ async function describePage(s: Session, reply: FastifyReply, schema: string, tab
178
178
  const t = table ? tables.find((x) => x.table === table) : undefined;
179
179
  const noted = new Set((await owner.query<{ t: string }>(`select distinct table_name as t from meta.ai_table_note where schema_name = $1`, [schema])).rows.map((r) => r.t));
180
180
  const value = (col: string, current: string | null) => (draft ? (col === '' ? draft.table : draft.columns?.[col]) ?? current ?? '' : current ?? '');
181
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai')}
181
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai', s)}
182
182
  <p><a href="${BASE}/sql/ai">◂ SQL Workshop AI</a></p>
183
183
  ${message ?? ''}
184
184
  ${region('Describe tables for AI', html`
@@ -225,7 +225,7 @@ async function pagesPage(s: Session, reply: FastifyReply, a: any, description: s
225
225
  const tables = (await wizardTables(a.db_role)).filter((t) => t.access).map((t) => t.t);
226
226
  const svc = await builderService();
227
227
  const kindOptions = WIZARD_KINDS.map(([k, label]): [string, string] => [k, label]);
228
- const main = html`${appHeader(a, 'pages')}
228
+ const main = html`${appHeader(a, 'pages', s)}
229
229
  ${message ?? ''}
230
230
  ${region('Create pages with AI', html`
231
231
  <p class="muted u-mt0">Describe the pages you want. The App Builder's AI service proposes pages of the create page wizards for tables and views the application's role (${a.db_role ?? 'the owner'}) can read; you check the proposals and create the ones you tick. Table and column names, types and <a href="${BASE}/sql/ai/describe">descriptions</a> are sent to the service, never rows.</p>
@@ -116,7 +116,7 @@ async function apiPage(s: Session, a: any, issued?: { token: string; username: s
116
116
  ].join('\n');
117
117
 
118
118
  const check = (ok: boolean, text: Raw | string) => html`<li>${ok ? '✓' : '✗'} ${text}</li>`;
119
- const main = html`${appHeader(a, 'api')}
119
+ const main = html`${appHeader(a, 'api', s)}
120
120
  <div class="columns">
121
121
  ${region('Setup', html`
122
122
  <p class="muted u-mt0">PostgREST runs next to pgkiln and serves the <code>${API_SCHEMA}</code> schema over HTTP. Requests carry a JWT whose <code>role</code> claim is this application’s API role; row level security uses the same <code>meta.app_user()</code> and <code>meta.has_role()</code> as the web pages.</p>
@@ -167,7 +167,7 @@ async function listPage(s: Session, q: Record<string, unknown>, problems: string
167
167
  <div class="buttons"><button class="btn btn-hot"${usable.length ? '' : raw(' disabled')}>${icon('bolt')} Enable</button></div>
168
168
  ${usable.length ? '' : html`<p class="muted">No application of this workspace has a database role to run as.</p>`}
169
169
  </form>`;
170
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}${restNav('autorest')}
170
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}${restNav('autorest')}
171
171
  <p class="muted">RESTful Services: publish a table or view as REST endpoints (AutoREST). Rows come paged as JSON, with filters; a primary key gives single-row GET, PUT and DELETE. Requests run as the publishing application's database role with the caller as <code>meta.app_user()</code>, so its grants and row level security apply. For tailored endpoints with your own SQL or PL/pgSQL, create a <a href="${BASE}/sql/rest/modules">module</a>.</p>
172
172
  ${region('Published objects', table)}
173
173
  ${region('Enable a table or view', form)}`;
@@ -227,7 +227,7 @@ async function detailPage(s: Session, o: Row, problems: string[] = []) {
227
227
  ].filter(Boolean).join('\n');
228
228
  const fetchEx = `const res = await fetch('${base}?limit=10'${o.auth === 'public' ? '' : `, {\n headers: { Authorization: \`Bearer \${token}\` },\n}`});\nconst { items, has_more } = await res.json();`;
229
229
  const openapi = `${publicUrl()}/a/${o.app_alias}/autorest/openapi.json`;
230
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}
230
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}
231
231
  <h2 class="u-mb1 wrap-anywhere">${o.schema_name}.${o.object_name} <span class="muted small">${info ? kindLabel(info.kind) : ''} · ${o.enabled ? 'enabled' : 'disabled'}</span></h2>
232
232
  ${errorList(problems)}
233
233
  ${!o.enabled ? html`<div class="alert alert-info" role="status">Disabled: the endpoints answer 404.</div>` : ''}
@@ -40,7 +40,7 @@ export async function availabilityRoutes(app: FastifyInstance) {
40
40
  'Run only: the builder refuses changes to the application (pages, shared components, settings, restores) until it is set back here. pgkiln import --replace still deploys it.')}
41
41
  </div>
42
42
  <div class="buttons"><button class="btn btn-hot">Save</button></div></form>`);
43
- const main = html`${appHeader(a, 'settings')}<div class="ide-body">${body}</div>`;
43
+ const main = html`${appHeader(a, 'settings', s)}<div class="ide-body">${body}</div>`;
44
44
  return send(reply, s, shell(s, `${a.name} availability`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Availability']], main));
45
45
  });
46
46
 
@@ -66,7 +66,7 @@ export async function backupRoutes(app: FastifyInstance) {
66
66
  <div class="form-grid">${input('keep', 'Backups to keep', a.backups_keep, { type: 'number', help: '0 to 500; 0 makes no backups at all (also not before an import).' })}</div>
67
67
  <div class="buttons"><button class="btn">Save</button></div></form>`)}
68
68
  </div>`;
69
- const main = html`${appHeader(a, 'pages')}<div class="ide-body">${body}</div>`;
69
+ const main = html`${appHeader(a, 'pages', s)}<div class="ide-body">${body}</div>`;
70
70
  return send(reply, s, shell(s, `${a.name} backups`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Backups']], main));
71
71
  });
72
72
 
@@ -106,7 +106,7 @@ async function listPage(s: Session, q: Record<string, unknown>) {
106
106
  ${exp ? html`<input type="hidden" name="app" value="${exp.id}">` : ''}
107
107
  <div class="buttons"><button class="btn btn-hot">${icon('download')} Download migration</button></div></form>`
108
108
  : '';
109
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('changes')}
109
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('changes', s)}
110
110
  <p class="muted">The change log (Liquibase, Flyway): DDL run from the SQL Worksheet, Table Properties and Schema Comparison is kept as change sets per schema, with who ran it and when. Download them as a migration for another database: Flyway versioned files, a Liquibase formatted SQL changelog or a plain script. Only statements that succeeded are kept; data changes (INSERT, UPDATE, DELETE) are not.</p>
111
111
  ${nav}
112
112
  ${schema ? region(`Change sets of ${schema}`, html`${exportPick}${table}${downloads}`) : region('Change sets', table)}`;
@@ -87,7 +87,7 @@ async function listPage(req: Req, reply: FastifyReply) {
87
87
  <small class="help">Names separated by commas or spaces; <code>*</code> for all, <code>ord*</code> for those starting with ord. You can add more later.</small></div>
88
88
  </div>
89
89
  <div class="buttons"><button class="btn btn-hot">${icon('plus')} Create diagram</button></div></form>`;
90
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
90
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
91
91
  <p class="muted u-mt0">Relational diagrams of a schema's tables and views: their columns, keys and the foreign keys between them. From a diagram: its DDL and a data dictionary.</p>
92
92
  ${region('Diagrams', list)}<div class="u-spacer"></div>${region('Create diagram', create)}`;
93
93
  return send(reply, s, shell(s, 'Data Modeler', [['SQL Workshop', `${BASE}/sql`], ['Data Modeler']], main, 'sql'));
@@ -102,7 +102,7 @@ async function diagramPage(req: Req, reply: FastifyReply) {
102
102
  const m = await model(s, d);
103
103
  const crumbs: [string, string?][] = [['SQL Workshop', `${BASE}/sql`], ['Data Modeler', M], [d.name]];
104
104
  if (!m) {
105
- const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler')}<div class="alert alert-error" role="alert">The schema ${d.schema_name} doesn't exist any more, or this workspace can't use it.</div>
105
+ const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler', s)}<div class="alert alert-error" role="alert">The schema ${d.schema_name} doesn't exist any more, or this workspace can't use it.</div>
106
106
  <form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="Delete the diagram ${d.name}?">${icon('trash')} Delete the diagram</button></form>`;
107
107
  return send(reply, s, shell(s, d.name, crumbs, main, 'sql'));
108
108
  }
@@ -151,7 +151,7 @@ async function diagramPage(req: Req, reply: FastifyReply) {
151
151
  const settings = html`<details class="dm-settings"><summary>Rename or delete the diagram</summary>
152
152
  <form method="post" action="${M}/${d.id}/rename" class="dm-search-row">${csrf(s)}<label class="sr-only" for="f_rename">Name</label><input id="f_rename" name="name" value="${d.name}" maxlength="120" required><button class="btn">${icon('edit')} Rename</button></form>
153
153
  <form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="Delete the diagram ${d.name}? The tables stay as they are.">${icon('trash')} Delete diagram</button></form></details>`;
154
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
154
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
155
155
  <div class="dm-head"><h2 class="dm-name">${d.name}</h2><span class="muted small">Schema ${d.schema_name} · ${tables.length} objects · updated ${when(d.updated_at)} by ${d.updated_by ?? d.created_by}</span></div>
156
156
  ${missing.length ? html`<div class="alert alert-error" role="alert">Not in ${d.schema_name} any more: ${missing.join(', ')}. Saving the diagram leaves them out.</div>` : ''}
157
157
  ${toolbar}
@@ -353,7 +353,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
353
353
  const script = diagramDdl(d.schema_name, new Set(m.tables.map((t) => t.name)), await readSchema(m.db.pool, d.schema_name));
354
354
  if (req.query.download === '1')
355
355
  return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}.sql"`).type('application/sql; charset=utf-8').send(script);
356
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
356
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
357
357
  ${region(`DDL of ${d.name}`, html`<p class="muted u-mt0">The CREATE statements of the diagram's ${m.tables.length} objects in schema ${d.schema_name}: tables, their keys, indexes, triggers, row level security, policies, comments and grants, and views. Review before running.</p>
358
358
  <pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL">${script}</pre>
359
359
  <form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${script}">
@@ -171,7 +171,7 @@ async function page(req: Req, reply: FastifyReply) {
171
171
  }
172
172
  const title = `Security of ${cur.schema}.${cur.table}`;
173
173
  const objUrl = `${BASE}/sql/objects?${new URLSearchParams({ schema: cur.schema, k: 'table', o: ref })}`;
174
- const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects')}${form(s, cur, def, { ref, sql, warnings, errors, output })}`;
174
+ const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}${form(s, cur, def, { ref, sql, warnings, errors, output })}`;
175
175
  return send(reply, s, shell(s, title, [['SQL Workshop', `${BASE}/sql`], ['Object Browser', `${BASE}/sql/objects`], [`${cur.schema}.${cur.table}`, objUrl], ['Security']], main, 'sql'));
176
176
  }
177
177
 
@@ -139,7 +139,7 @@ export function resultHtml(r: LoadResult, failed: boolean) {
139
139
 
140
140
  export async function dataLoadRoutes(app: FastifyInstance) {
141
141
  const page = (s: Session, reply: FastifyReply, main: Raw) =>
142
- send(reply, s, shell(s, 'Load Data', [['SQL Workshop', `${BASE}/sql`], ['Load Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('load')}${main}`, 'sql'));
142
+ send(reply, s, shell(s, 'Load Data', [['SQL Workshop', `${BASE}/sql`], ['Load Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('load', s)}${main}`, 'sql'));
143
143
 
144
144
  // step 1: choose a file
145
145
  app.get(`${BASE}/sql/load`, async (req: Req, reply) => {
@@ -36,7 +36,7 @@ async function admin(req: Req, reply: FastifyReply) {
36
36
  }
37
37
 
38
38
  const page = (reply: FastifyReply, s: Session, title: string, trail: [string, string?][], body: Raw, code = 200) =>
39
- send(reply.code(code), s, shell(s, title, crumbs(...trail), html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('datapump')}${body}`, 'sql'));
39
+ send(reply.code(code), s, shell(s, title, crumbs(...trail), html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('datapump', s)}${body}`, 'sql'));
40
40
 
41
41
  export const size = (n: number | null) =>
42
42
  n === null ? '' : n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MB` : `${(n / 1024 ** 3).toFixed(2)} GB`;
@@ -65,7 +65,7 @@ export async function ddlToolRoutes(app: FastifyInstance) {
65
65
  result = html`<div class="alert alert-error" role="alert">${(e as Error).message}</div>`;
66
66
  }
67
67
  }
68
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ddl')}
68
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ddl', s)}
69
69
  <p class="muted">The DDL of a schema's objects, from the database catalog, in an order that runs: types, sequences, tables, functions, views, constraints, indexes, triggers, row level security, comments and grants (APEX: Generate DDL). Objects of extensions are left out.</p>
70
70
  <form method="get" class="u-mb1">
71
71
  <div class="form-grid">${schemaSelect('schema', 'Schema', schemas, schema)}</div>
@@ -114,7 +114,7 @@ export async function ddlToolRoutes(app: FastifyInstance) {
114
114
  result = html`<div class="alert alert-error" role="alert">${(e as Error).message}</div>`;
115
115
  }
116
116
  }
117
- const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('compare')}
117
+ const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('compare', s)}
118
118
  <p class="muted">Compares the objects of two schemas of this database by their DDL (APEX: Schema Comparison), e.g. a development copy with the schema it came from, and writes a script that makes the target like the source. The order of a table's columns is not compared.</p>
119
119
  <form method="get" class="u-mb1">
120
120
  <div class="form-grid">${schemaSelect('source', 'Source', schemas, source)}${schemaSelect('target', 'Target (to make like the source)', schemas, target)}</div>