pgkiln 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/SECURITY.md +11 -9
  2. package/db/migrations/087_login_page.sql +175 -0
  3. package/db/migrations/088_region_templates.sql +18 -0
  4. package/db/migrations/089_page_groups.sql +330 -0
  5. package/db/migrations/090_workspace_isolation.sql +512 -0
  6. package/docs/apex-feature-parity.md +23 -21
  7. package/docs/guide/03-builder.md +85 -8
  8. package/docs/guide/04-pages-and-regions.md +42 -3
  9. package/docs/guide/08-security.md +46 -5
  10. package/docs/guide/11-from-apex.md +4 -1
  11. package/docs/guide/12-development.md +13 -3
  12. package/docs/guide/17-mobile.md +3 -1
  13. package/docs/guide/18-cli.md +2 -0
  14. package/docs/guide/20-ai-agents.md +2 -0
  15. package/examples/hr/hr_40_sample_data.sql +1 -1
  16. package/examples/hr/hr_54_region_templates.sql +67 -0
  17. package/examples/hr/hr_55_page_groups.sql +24 -0
  18. package/package.json +1 -1
  19. package/public/app.css +60 -3
  20. package/public/app.js +89 -3
  21. package/public/builder.css +11 -0
  22. package/public/builder.js +50 -0
  23. package/public/code-editor.css +3 -3
  24. package/public/sw.js +5 -3
  25. package/src/app.ts +20 -0
  26. package/src/appfiles.ts +1 -0
  27. package/src/automations.ts +3 -2
  28. package/src/builder/advisor.ts +25 -5
  29. package/src/builder/ai-builder.ts +27 -18
  30. package/src/builder/blueprints.ts +7 -7
  31. package/src/builder/code-editor.ts +3 -2
  32. package/src/builder/components.ts +15 -2
  33. package/src/builder/dataload.ts +24 -14
  34. package/src/builder/ddl-tools.ts +117 -0
  35. package/src/builder/designer.ts +29 -3
  36. package/src/builder/documents.ts +2 -1
  37. package/src/builder/forms.ts +6 -4
  38. package/src/builder/home.ts +6 -2
  39. package/src/builder/login-page.ts +82 -0
  40. package/src/builder/newapp.ts +21 -3
  41. package/src/builder/quicksql.ts +3 -2
  42. package/src/builder/region-help.ts +4 -2
  43. package/src/builder/region-settings.ts +19 -3
  44. package/src/builder/report-settings.ts +4 -2
  45. package/src/builder/routes.ts +18 -6
  46. package/src/builder/sampledata.ts +22 -18
  47. package/src/builder/scripts.ts +27 -16
  48. package/src/builder/shared.ts +1 -1
  49. package/src/builder/sql.ts +18 -10
  50. package/src/builder/ui.ts +2 -2
  51. package/src/builder/unload.ts +3 -2
  52. package/src/builder/workspaces.ts +175 -15
  53. package/src/cli/main.ts +45 -0
  54. package/src/cli/mcp.ts +42 -0
  55. package/src/cli/replace.ts +4 -0
  56. package/src/db.ts +36 -6
  57. package/src/ddl.ts +510 -0
  58. package/src/debug.ts +3 -2
  59. package/src/i18n/ar.ts +17 -0
  60. package/src/i18n/cs.ts +17 -0
  61. package/src/i18n/da.ts +17 -0
  62. package/src/i18n/de.ts +17 -0
  63. package/src/i18n/el.ts +17 -0
  64. package/src/i18n/es.ts +17 -0
  65. package/src/i18n/fi.ts +17 -0
  66. package/src/i18n/fr.ts +17 -0
  67. package/src/i18n/he.ts +17 -0
  68. package/src/i18n/it.ts +17 -0
  69. package/src/i18n/ja.ts +17 -0
  70. package/src/i18n/ko.ts +17 -0
  71. package/src/i18n/nb.ts +17 -0
  72. package/src/i18n/pl.ts +17 -0
  73. package/src/i18n/pt.ts +17 -0
  74. package/src/i18n/ru.ts +17 -0
  75. package/src/i18n/sv.ts +17 -0
  76. package/src/i18n/tr.ts +17 -0
  77. package/src/i18n/uk.ts +17 -0
  78. package/src/i18n/zh.ts +17 -0
  79. package/src/i18n.ts +34 -0
  80. package/src/icons.ts +2 -1
  81. package/src/layout.ts +3 -2
  82. package/src/metadata.ts +10 -4
  83. package/src/migrate.ts +3 -0
  84. package/src/process-jobs.ts +7 -2
  85. package/src/restsync.ts +3 -2
  86. package/src/runtime/devbar.ts +3 -1
  87. package/src/runtime/display-selector.ts +12 -4
  88. package/src/runtime/grid.ts +170 -26
  89. package/src/runtime/lists.ts +11 -1
  90. package/src/runtime/password-reset.ts +171 -0
  91. package/src/runtime/pwa.ts +7 -10
  92. package/src/runtime/regions.ts +33 -0
  93. package/src/runtime/render.ts +15 -0
  94. package/src/runtime/report-views.ts +4 -2
  95. package/src/runtime/report.ts +36 -22
  96. package/src/runtime/routes.ts +39 -33
  97. package/src/runtime/signin.ts +80 -0
  98. package/src/sampledata.ts +3 -3
  99. package/src/session.ts +5 -2
  100. package/src/static-version.ts +34 -0
  101. package/src/websources.ts +3 -1
  102. package/src/workflow.ts +65 -9
  103. package/src/workspace-db.ts +166 -0
package/src/cli/main.ts CHANGED
@@ -133,6 +133,51 @@ const COMMANDS: Record<string, Command> = {
133
133
  },
134
134
  },
135
135
 
136
+ ddl: {
137
+ usage: 'pgkiln ddl <schema> [--no-comments] [--no-grants] [--out <file>]',
138
+ summary: 'write the DDL of a schema\'s objects (SQL Workshop → Generate DDL)',
139
+ details:
140
+ 'Types, sequences, tables, functions, views, constraints, indexes, triggers, row level security,\n' +
141
+ 'comments and grants, in an order that runs, with the schema in search_path at the top (so the\n' +
142
+ 'script also creates the objects in another schema). Objects of extensions are left out.',
143
+ options: { 'no-comments': { type: 'boolean' }, 'no-grants': { type: 'boolean' }, out: { type: 'string', short: 'o' } },
144
+ optionHelp: [['--no-comments', 'leave out comments'], ['--no-grants', 'leave out grants'], ['-o, --out <file>', 'write to a file instead of standard output']],
145
+ positionals: [1, 1],
146
+ async run(v, [schema]) {
147
+ const { ddlScript, OBJECT_TYPES, pick, readSchema } = await import('../ddl.ts');
148
+ const objects = await withDb((db) => readSchema(db, schema));
149
+ const script = ddlScript(schema, pick(objects, new Set(OBJECT_TYPES), { comments: !v['no-comments'], grants: !v['no-grants'] }));
150
+ if (v.out) {
151
+ writeFileSync(v.out as string, script);
152
+ err(`Wrote the DDL of ${schema} to ${v.out}.\n`);
153
+ } else out(script);
154
+ return EXIT.ok;
155
+ },
156
+ },
157
+
158
+ compare: {
159
+ usage: 'pgkiln compare <source> <target> [--script]',
160
+ summary: 'compare two schemas (SQL Workshop → Schema Comparison); exit code 1 when they differ',
161
+ details:
162
+ 'Lists what only the source has (+), what only the target has (-) and what differs (~), by their DDL;\n' +
163
+ 'the order of a table\'s columns is not compared. --script writes the script that makes the target\n' +
164
+ 'like the source instead (drops of what only the target has are commented out). Nothing is run.',
165
+ options: { script: { type: 'boolean' } },
166
+ optionHelp: [['--script', 'write the change script']],
167
+ positionals: [2, 2],
168
+ async run(v, [source, target]) {
169
+ const { changeScript, compare, readSchema } = await import('../ddl.ts');
170
+ const [a, b] = await withDb(async (db) => [await readSchema(db, source), await readSchema(db, target)]);
171
+ const { differences, same } = compare(a, b);
172
+ if (v.script) out(changeScript(source, target, a, b));
173
+ else {
174
+ for (const d of differences) out(`${d.state === 'source' ? '+' : d.state === 'target' ? '-' : '~'} ${d.kind} ${d.name}\n`);
175
+ err(differences.length ? `${differences.length} difference(s), ${same} object(s) the same.\n` : `The schemas are the same (${same} objects).\n`);
176
+ }
177
+ return differences.length ? EXIT.differences : EXIT.ok;
178
+ },
179
+ },
180
+
136
181
  export: {
137
182
  usage: 'pgkiln export <alias> [--format json|dir|text] [--out <path>]',
138
183
  summary: 'export an application as one JSON file or as a directory with a file per component',
package/src/cli/mcp.ts CHANGED
@@ -473,6 +473,48 @@ export const TOOLS: Tool[] = [
473
473
  });
474
474
  },
475
475
  },
476
+ {
477
+ name: 'generate_ddl',
478
+ title: 'Generate the DDL of a schema',
479
+ description:
480
+ 'The DDL of a schema\'s objects from the catalog (types, sequences, tables, functions, views, constraints, indexes, triggers, ' +
481
+ 'row level security, comments and grants), in an order that runs, with the schema in search_path at the top. ' +
482
+ 'Use it to start a migration script or to see how objects are defined; nothing is run.',
483
+ inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'the schema, e.g. sales' } }, required: ['schema'] },
484
+ annotations: { readOnlyHint: true },
485
+ async run(a) {
486
+ const schema = str(a, 'schema');
487
+ if (!schema) throw new ToolError('schema is required');
488
+ const { ddlScript, readSchema } = await import('../ddl.ts');
489
+ return withDb(async (db) => cap(ddlScript(schema, await readSchema(db, schema))));
490
+ },
491
+ },
492
+ {
493
+ name: 'compare_schemas',
494
+ title: 'Compare two schemas',
495
+ description:
496
+ 'Compares the objects of two schemas of the database by their DDL: what only the source has (+), only the target has (-), ' +
497
+ 'and what differs (~), followed by a script that makes the target like the source (drops commented out). Nothing is run.',
498
+ inputSchema: {
499
+ type: 'object',
500
+ properties: { source: { type: 'string', description: 'the schema to compare with, e.g. sales' }, target: { type: 'string', description: 'the schema to make like the source, e.g. sales_dev' } },
501
+ required: ['source', 'target'],
502
+ },
503
+ annotations: { readOnlyHint: true },
504
+ async run(a) {
505
+ const source = str(a, 'source');
506
+ const target = str(a, 'target');
507
+ if (!source || !target) throw new ToolError('source and target are required');
508
+ const { changeScript, compare, readSchema } = await import('../ddl.ts');
509
+ return withDb(async (db) => {
510
+ const [x, y] = [await readSchema(db, source), await readSchema(db, target)];
511
+ const { differences, same } = compare(x, y);
512
+ if (!differences.length) return `The schemas are the same (${same} objects).\n`;
513
+ const list = differences.map((d) => `${d.state === 'source' ? '+' : d.state === 'target' ? '-' : '~'} ${d.kind} ${d.name}`).join('\n');
514
+ return cap(`${differences.length} difference(s), ${same} object(s) the same:\n${list}\n\n${changeScript(source, target, x, y)}`);
515
+ });
516
+ },
517
+ },
476
518
  {
477
519
  name: 'run_query',
478
520
  title: 'Run a read-only query',
@@ -26,9 +26,13 @@ export const REPLACED = [
26
26
  'search_config', 'email_template',
27
27
  // (086) application settings (installed values kept, see below), dynamic translations
28
28
  'app_setting', 'dynamic_translation',
29
+ // (089) page groups
30
+ 'page_group',
29
31
  ];
30
32
  /** Tables of an application that belong to the installation: kept. */
31
33
  export const KEPT = ['app_access', 'api_client', 'session', 'sso_pending', 'saved_report', 'persistent_login', 'task', 'workflow', 'process_job',
34
+ // (087) password reset links of this installation's accounts
35
+ 'password_reset',
32
36
  // (042) builder state of this installation: locks and developer comments (by page number)
33
37
  'builder_lock', 'dev_comment',
34
38
  // (051) debug messages recorded by this installation
package/src/db.ts CHANGED
@@ -13,7 +13,7 @@ const inTx = new AsyncLocalStorage<ReadonlySet<string>>();
13
13
  /** Waiting for a free connection fails after this, instead of hanging the request forever. */
14
14
  const ACQUIRE_TIMEOUT_MS = Number(process.env.DB_ACQUIRE_TIMEOUT_MS ?? 30_000);
15
15
 
16
- function makePool(connectionString: string | undefined, name: string) {
16
+ export function makePool(connectionString: string | undefined, name: string, max = Number(process.env.DB_POOL_SIZE ?? 10)) {
17
17
  const opts = {
18
18
  connectionString,
19
19
  application_name: name,
@@ -21,7 +21,7 @@ function makePool(connectionString: string | undefined, name: string) {
21
21
  // a transaction left open (a bug, a hung client) is ended by the database and frees its connection
22
22
  idle_in_transaction_session_timeout: Number(process.env.DB_IDLE_IN_TRANSACTION_MS ?? 300_000),
23
23
  };
24
- const pool = new pg.Pool({ ...opts, max: Number(process.env.DB_POOL_SIZE ?? 10) });
24
+ const pool = new pg.Pool({ ...opts, max });
25
25
  // A query issued while this async chain already holds a connection of this pool
26
26
  // (in tx()) must not wait for a second one from the same pool: with every
27
27
  // connection held by requests doing that, none would ever be released. Those
@@ -72,11 +72,36 @@ if (!process.env.RUNTIME_DATABASE_URL)
72
72
 
73
73
  /** Owner connection: builder, SQL Workshop, migrations. */
74
74
  export const owner = makePool(ownerUrl, 'pgkiln-builder');
75
- /** Least-privilege connection (pgkiln_runtime) that runs applications. */
76
- export const runtime = makePool(process.env.RUNTIME_DATABASE_URL ?? ownerUrl, 'pgkiln-runtime');
75
+ export type Db = ReturnType<typeof makePool>;
76
+
77
+ export const runtimeUrl = process.env.RUNTIME_DATABASE_URL ?? ownerUrl;
78
+ /** The shared least-privilege connection (pgkiln_runtime): applications outside isolated workspaces. */
79
+ export const sharedRuntime = makePool(runtimeUrl, 'pgkiln-runtime');
80
+
81
+ /**
82
+ * The runtime connection of the current request or job: an isolated workspace's
83
+ * own login (src/workspace-db.ts sets it with runtimeScope.run) or the shared one.
84
+ */
85
+ export const runtimeScope = new AsyncLocalStorage<Db>();
86
+ const currentRuntime = () => runtimeScope.getStore() ?? sharedRuntime;
87
+
88
+ /** Least-privilege connection that runs applications: the current scope's (see runtimeScope). */
89
+ export const runtime: Db = {
90
+ get pool() {
91
+ return currentRuntime().pool;
92
+ },
93
+ query: (sql, params) => currentRuntime().query(sql, params),
94
+ one: (sql, params) => currentRuntime().one(sql, params),
95
+ tx: (fn) => currentRuntime().tx(fn),
96
+ end: () => sharedRuntime.end(),
97
+ };
98
+
99
+ const closers: (() => Promise<void>)[] = [];
100
+ /** Pools made elsewhere (the workspaces' own) close with the others. */
101
+ export const onClosePools = (fn: () => Promise<void>) => closers.push(fn);
77
102
 
78
103
  export async function closePools() {
79
- await Promise.all([owner.end(), runtime.end()]);
104
+ await Promise.all([owner.end(), sharedRuntime.end(), ...closers.map((f) => f())]);
80
105
  }
81
106
 
82
107
  export interface AppContext {
@@ -123,8 +148,13 @@ export async function appTx<T>(ctx: AppContext, fn: (c: Client) => Promise<T>):
123
148
 
124
149
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
125
150
 
151
+ let appRuntime: (appId: number) => Promise<Db> = async () => currentRuntime();
152
+ /** How appTx finds an application's runtime connection (src/workspace-db.ts: an isolated workspace's own). */
153
+ export const setAppRuntime = (fn: (appId: number) => Promise<Db>) => (appRuntime = fn);
154
+
126
155
  async function appTxInner<T>(ctx: AppContext, fn: (c: Client) => Promise<T>): Promise<T> {
127
- return runtime.tx(async (c) => {
156
+ const db = await appRuntime(ctx.appId);
157
+ return db.tx(async (c) => {
128
158
  await c.query(
129
159
  `select set_config('pgkiln.app_user', $1, true),
130
160
  set_config('pgkiln.session_id', $2, true),