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.
- package/SECURITY.md +11 -9
- 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/db/migrations/090_workspace_isolation.sql +512 -0
- package/docs/apex-feature-parity.md +23 -21
- package/docs/guide/03-builder.md +85 -8
- package/docs/guide/04-pages-and-regions.md +42 -3
- package/docs/guide/08-security.md +46 -5
- package/docs/guide/11-from-apex.md +4 -1
- package/docs/guide/12-development.md +13 -3
- package/docs/guide/17-mobile.md +3 -1
- package/docs/guide/18-cli.md +2 -0
- package/docs/guide/20-ai-agents.md +2 -0
- package/examples/hr/hr_40_sample_data.sql +1 -1
- package/examples/hr/hr_54_region_templates.sql +67 -0
- package/examples/hr/hr_55_page_groups.sql +24 -0
- package/package.json +1 -1
- package/public/app.css +60 -3
- package/public/app.js +89 -3
- package/public/builder.css +11 -0
- package/public/builder.js +50 -0
- package/public/code-editor.css +3 -3
- package/public/sw.js +5 -3
- package/src/app.ts +20 -0
- package/src/appfiles.ts +1 -0
- package/src/automations.ts +3 -2
- package/src/builder/advisor.ts +25 -5
- package/src/builder/ai-builder.ts +27 -18
- package/src/builder/blueprints.ts +7 -7
- package/src/builder/code-editor.ts +3 -2
- package/src/builder/components.ts +15 -2
- package/src/builder/dataload.ts +24 -14
- package/src/builder/ddl-tools.ts +117 -0
- package/src/builder/designer.ts +29 -3
- package/src/builder/documents.ts +2 -1
- package/src/builder/forms.ts +6 -4
- package/src/builder/home.ts +6 -2
- package/src/builder/login-page.ts +82 -0
- package/src/builder/newapp.ts +21 -3
- package/src/builder/quicksql.ts +3 -2
- package/src/builder/region-help.ts +4 -2
- package/src/builder/region-settings.ts +19 -3
- package/src/builder/report-settings.ts +4 -2
- package/src/builder/routes.ts +18 -6
- package/src/builder/sampledata.ts +22 -18
- package/src/builder/scripts.ts +27 -16
- package/src/builder/shared.ts +1 -1
- package/src/builder/sql.ts +18 -10
- package/src/builder/ui.ts +2 -2
- package/src/builder/unload.ts +3 -2
- package/src/builder/workspaces.ts +175 -15
- package/src/cli/main.ts +45 -0
- package/src/cli/mcp.ts +42 -0
- package/src/cli/replace.ts +4 -0
- package/src/db.ts +36 -6
- package/src/ddl.ts +510 -0
- package/src/debug.ts +3 -2
- package/src/i18n/ar.ts +17 -0
- package/src/i18n/cs.ts +17 -0
- package/src/i18n/da.ts +17 -0
- package/src/i18n/de.ts +17 -0
- package/src/i18n/el.ts +17 -0
- package/src/i18n/es.ts +17 -0
- package/src/i18n/fi.ts +17 -0
- package/src/i18n/fr.ts +17 -0
- package/src/i18n/he.ts +17 -0
- package/src/i18n/it.ts +17 -0
- package/src/i18n/ja.ts +17 -0
- package/src/i18n/ko.ts +17 -0
- package/src/i18n/nb.ts +17 -0
- package/src/i18n/pl.ts +17 -0
- package/src/i18n/pt.ts +17 -0
- package/src/i18n/ru.ts +17 -0
- package/src/i18n/sv.ts +17 -0
- package/src/i18n/tr.ts +17 -0
- package/src/i18n/uk.ts +17 -0
- package/src/i18n/zh.ts +17 -0
- package/src/i18n.ts +34 -0
- package/src/icons.ts +2 -1
- package/src/layout.ts +3 -2
- package/src/metadata.ts +10 -4
- package/src/migrate.ts +3 -0
- package/src/process-jobs.ts +7 -2
- package/src/restsync.ts +3 -2
- package/src/runtime/devbar.ts +3 -1
- package/src/runtime/display-selector.ts +12 -4
- package/src/runtime/grid.ts +170 -26
- package/src/runtime/lists.ts +11 -1
- package/src/runtime/password-reset.ts +171 -0
- package/src/runtime/pwa.ts +7 -10
- package/src/runtime/regions.ts +33 -0
- package/src/runtime/render.ts +15 -0
- package/src/runtime/report-views.ts +4 -2
- package/src/runtime/report.ts +36 -22
- package/src/runtime/routes.ts +39 -33
- package/src/runtime/signin.ts +80 -0
- package/src/sampledata.ts +3 -3
- package/src/session.ts +5 -2
- package/src/static-version.ts +34 -0
- package/src/websources.ts +3 -1
- package/src/workflow.ts +65 -9
- 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',
|
package/src/cli/replace.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
76
|
-
|
|
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(),
|
|
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
|
-
|
|
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),
|