pgkiln 0.56.0 → 0.58.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 +1 -0
- package/docs/apex-feature-parity.md +1 -1
- package/docs/guide/03-builder.md +7 -3
- package/docs/guide/12-development.md +16 -1
- package/package.json +1 -1
- package/public/builder.css +1 -1
- package/public/worksheet.js +31 -26
- package/src/builder/i18n/de.ts +711 -0
- package/src/builder/i18n/en.ts +711 -0
- package/src/builder/i18n/es.ts +711 -0
- package/src/builder/i18n/fr.ts +711 -0
- package/src/builder/i18n/nl.ts +711 -0
- package/src/builder/i18n/pt.ts +711 -0
- package/src/builder/i18n.ts +10 -0
- package/src/builder/object-browser.ts +71 -67
- package/src/builder/routes.ts +88 -87
- package/src/builder/shared.ts +86 -77
- package/src/builder/table-editor.ts +51 -48
- package/src/builder/ui.ts +3 -2
- package/src/builder/worksheet.ts +160 -154
package/src/builder/i18n.ts
CHANGED
|
@@ -86,6 +86,16 @@ const LABELS: Record<string, BuilderKey> = {
|
|
|
86
86
|
Settings: 'app.tab.settings',
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* (sprint 123) The texts the builder's own JavaScript shows (keys js.*), in the language: shell() puts them in the
|
|
91
|
+
* page as JSON in an attribute (<div id="builder-texts" hidden data-texts="…">, escaped like any value; no inline
|
|
92
|
+
* script), and public/*.js read them with a small helper. English fills any gap.
|
|
93
|
+
*/
|
|
94
|
+
export function clientTexts(lang: BuilderLang): Record<string, string> {
|
|
95
|
+
const own = MESSAGES[lang];
|
|
96
|
+
return Object.fromEntries(Object.keys(en).filter((k) => k.startsWith('js.')).map((k) => [k, own[k] ?? (en as Record<string, string>)[k]]));
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
/** A translator: the language's messages (`own`, for tests: a partial file), then English, then the key itself. */
|
|
90
100
|
export function builderTranslator(lang: BuilderLang, own: Readonly<Record<string, string>> = MESSAGES[lang]): BuilderTranslate {
|
|
91
101
|
const text = (key: string): string | undefined => own[key] ?? (en as Record<string, string>)[key];
|
|
@@ -3,6 +3,7 @@ import pg from 'pg';
|
|
|
3
3
|
import { html, raw, type Raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import { readSchema } from '../ddl.ts';
|
|
6
|
+
import { tr } from './i18n.ts';
|
|
6
7
|
import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Req } from './ui.ts';
|
|
7
8
|
import { currentWorkspace, inWorkspaceSql, sessionSchema, workshopDb } from './workspaces.ts';
|
|
8
9
|
import { objectMenu } from './object-actions.ts';
|
|
@@ -50,6 +51,7 @@ const TABLE_TABS = [
|
|
|
50
51
|
['columns', 'Columns'], ['data', 'Data'], ['indexes', 'Indexes'], ['constraints', 'Constraints'], ['triggers', 'Triggers'],
|
|
51
52
|
['grants', 'Grants'], ['rls', 'Row level security'], ['dependencies', 'Dependencies'], ['statistics', 'Statistics'], ['ddl', 'DDL'],
|
|
52
53
|
] as const;
|
|
54
|
+
type TabKey = (typeof TABLE_TABS)[number][0];
|
|
53
55
|
|
|
54
56
|
/** CREATE statements the + menu and the tiles start with (the SQL Worksheet shows them; nothing runs until Run). */
|
|
55
57
|
export function createTemplate(kind: string, schema: string): string | null {
|
|
@@ -110,6 +112,7 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
110
112
|
app.get(`${BASE}/sql/objects`, async (req: Req, reply) => {
|
|
111
113
|
const s = await developer(req, reply);
|
|
112
114
|
if (!s) return;
|
|
115
|
+
const tx = tr(s);
|
|
113
116
|
const db = await workshopDb(s);
|
|
114
117
|
const q = req.query ?? {};
|
|
115
118
|
const system = q.system === '1';
|
|
@@ -131,18 +134,18 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
131
134
|
const link = (extra: Record<string, string>) => `?${new URLSearchParams({ schema, ...(system ? { system: '1' } : {}), ...(filter ? { q: filter } : {}), ...extra })}`;
|
|
132
135
|
|
|
133
136
|
const tree = html`<form method="get" class="ob-filter" role="search"><input type="hidden" name="schema" value="${schema}">${system ? html`<input type="hidden" name="system" value="1">` : ''}
|
|
134
|
-
<label class="sr-only" for="ob-q"
|
|
137
|
+
<label class="sr-only" for="ob-q">${tx('ob.filter_label')}</label><input id="ob-q" name="q" type="search" value="${filter}" placeholder="${tx('ob.filter')}" data-filter-tree="ob-tree"></form>
|
|
135
138
|
<div id="ob-tree">${OB_TYPES.map(([t, label, ic]) => {
|
|
136
139
|
const list = shown.filter((o) => o.type === t);
|
|
137
140
|
if (!list.length && filter) return '';
|
|
138
141
|
return html`<details class="ob-folder"${list.length && (filter || list.length <= 50 || selected?.type === t) ? raw(' open') : ''}>
|
|
139
|
-
<summary>${icon(ic)}<span>${label}</span><span class="kind">${list.length}</span></summary>
|
|
140
|
-
<ul class="tree">${list.map((o) => html`<li class="ob-item"><a href="${link({ k: o.type, o: o.ref })}"${selected === o ? raw(' aria-current="page"') : ''}>${icon(ic)}<span>${o.name}</span>${o.rls ? html`<span class="tag">RLS</span>` : ''}${o.ext ? html`<span class="tag"
|
|
142
|
+
<summary>${icon(ic)}<span>${tx.dyn(`ob.type.${t}`, label)}</span><span class="kind">${list.length}</span></summary>
|
|
143
|
+
<ul class="tree">${list.map((o) => html`<li class="ob-item"><a href="${link({ k: o.type, o: o.ref })}"${selected === o ? raw(' aria-current="page"') : ''}>${icon(ic)}<span>${o.name}</span>${o.rls ? html`<span class="tag">RLS</span>` : ''}${o.ext ? html`<span class="tag">${tx('ob.extension')}</span>` : ''}</a>${objectMenu(schema, o)}</li>`)}</ul>
|
|
141
144
|
</details>`;
|
|
142
145
|
})}</div>`;
|
|
143
146
|
|
|
144
147
|
let detail: Raw;
|
|
145
|
-
if (!schema) detail = html`<p class="muted"
|
|
148
|
+
if (!schema) detail = html`<p class="muted">${tx('ob.no_schema')}</p>`;
|
|
146
149
|
else if (selected) {
|
|
147
150
|
// (sprint 96) pin the object to the SQL Workshop's home
|
|
148
151
|
const href = `${BASE}/sql/objects?${new URLSearchParams({ schema, k: selected.type, o: selected.ref })}`;
|
|
@@ -150,33 +153,33 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
150
153
|
detail = html`<div class="ob-pin">${pin}</div>${await objectDetail(db, s, schema, selected, String(q.tab ?? ''), Number(q.p) || 1, link)}`;
|
|
151
154
|
}
|
|
152
155
|
else
|
|
153
|
-
detail = region(
|
|
154
|
-
<a class="create-source" href="${BASE}/sql/objects/table?${new URLSearchParams({ schema })}">${icon('table')}<span><b
|
|
155
|
-
[
|
|
156
|
-
|
|
157
|
-
<a class="create-source" href="${BASE}/sql/
|
|
158
|
-
<
|
|
159
|
-
<p class="muted">Select an object in the tree, or filter it by name.</p>`);
|
|
156
|
+
detail = region(tx('ob.create_in', { schema }), html`<div class="ob-tiles">
|
|
157
|
+
<a class="create-source" href="${BASE}/sql/objects/table?${new URLSearchParams({ schema })}">${icon('table')}<span><b>${tx('ob.tile.table_form')}</b><small class="muted">${tx('ob.tile.table_form.desc')}</small></span></a>${(['table', 'view', 'function', 'sequence', 'index'] as const).map((k) => [k, { table: 'table', view: 'layers', function: 'code', sequence: 'hash', index: 'list' }[k]] as const)
|
|
158
|
+
.map(([k, ic]) => html`<a class="create-source" href="${BASE}/sql/worksheet?${new URLSearchParams({ template: k, schema })}">${icon(ic)}<span><b>${tx(`ob.kind.${k}`)}</b><small class="muted">${tx('ob.tile.start', { what: k.toUpperCase() })}</small></span></a>`)}
|
|
159
|
+
<a class="create-source" href="${BASE}/sql/quick">${icon('bolt')}<span><b>${tx('ob.tile.quick')}</b><small class="muted">${tx('ob.tile.quick.desc')}</small></span></a>
|
|
160
|
+
<a class="create-source" href="${BASE}/sql/load">${icon('upload')}<span><b>${tx('ob.tile.load')}</b><small class="muted">${tx('ob.tile.load.desc')}</small></span></a></div>
|
|
161
|
+
<p class="muted">${tx('ob.select_hint')}</p>`);
|
|
160
162
|
|
|
161
163
|
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('objects', s)}
|
|
162
164
|
<div class="ob-head">
|
|
163
165
|
<form method="get" class="ob-schema">${filter ? html`<input type="hidden" name="q" value="${filter}">` : ''}
|
|
164
|
-
<label class="label" for="ob-schema"
|
|
166
|
+
<label class="label" for="ob-schema">${tx('ob.schema')}</label>
|
|
165
167
|
<select id="ob-schema" name="schema" data-autosubmit>${schemas.map((n) => html`<option value="${n}"${n === schema ? raw(' selected') : ''}>${n}</option>`)}</select>
|
|
166
|
-
<label class="check"><input type="checkbox" name="system" value="1"${system ? raw(' checked') : ''} data-autosubmit>
|
|
167
|
-
<button class="btn btn-sm"
|
|
168
|
-
<details class="menu ob-create"><summary class="btn btn-hot">${icon('plus')}
|
|
169
|
-
<div class="menu-panel align-right"><div class="menu-section menu-links">${['table', 'view', 'function', 'sequence', 'index'].map((k) =>
|
|
170
|
-
html`<a href="${BASE}/sql/worksheet?${new URLSearchParams({ template: k, schema })}">${icon('plus')} ${
|
|
168
|
+
<label class="check"><input type="checkbox" name="system" value="1"${system ? raw(' checked') : ''} data-autosubmit> ${tx('ob.system')}</label>
|
|
169
|
+
<button class="btn btn-sm">${tx('common.show')}</button></form>
|
|
170
|
+
<details class="menu ob-create"><summary class="btn btn-hot">${icon('plus')} ${tx('ob.create')}</summary>
|
|
171
|
+
<div class="menu-panel align-right"><div class="menu-section menu-links">${(['table', 'view', 'function', 'sequence', 'index'] as const).map((k) =>
|
|
172
|
+
html`<a href="${BASE}/sql/worksheet?${new URLSearchParams({ template: k, schema })}">${icon('plus')} ${tx(`ob.kind.${k}`)}</a>`)}</div></div></details>
|
|
171
173
|
</div>
|
|
172
|
-
<div class="designer"><aside class="region region-standard ob-tree" aria-label="
|
|
173
|
-
return send(reply, s, shell(s, '
|
|
174
|
+
<div class="designer"><aside class="region region-standard ob-tree" aria-label="${tx('ob.db_objects')}">${tree}</aside><div>${detail}</div></div>`;
|
|
175
|
+
return send(reply, s, shell(s, tx('ob.title'), [['SQL Workshop', `${BASE}/sql`], [tx('ob.title')]], main, 'sql'));
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
// column actions: the ALTER TABLE statement, into the SQL Worksheet to review and run
|
|
177
179
|
app.post(`${BASE}/sql/objects/alter`, async (req: Req, reply) => {
|
|
178
180
|
const s = await developer(req, reply);
|
|
179
181
|
if (!s) return;
|
|
182
|
+
const tx = tr(s);
|
|
180
183
|
const db = await workshopDb(s);
|
|
181
184
|
const b = req.body ?? {};
|
|
182
185
|
const table = await db.one<{ q: string; ok: boolean }>(
|
|
@@ -188,7 +191,7 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
188
191
|
).catch(() => undefined);
|
|
189
192
|
const backTo = `${BASE}/sql/objects?${new URLSearchParams({ schema: String(b.schema ?? ''), k: 'table', o: String(b.table ?? ''), tab: 'columns' })}`;
|
|
190
193
|
if (!table?.ok) {
|
|
191
|
-
flash(s, '
|
|
194
|
+
flash(s, tx('ob.err.table'), 'error');
|
|
192
195
|
return back(reply, s, `${BASE}/sql/objects`);
|
|
193
196
|
}
|
|
194
197
|
const col = String(b.column ?? '').trim();
|
|
@@ -198,14 +201,14 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
198
201
|
let problem = '';
|
|
199
202
|
if (b.action === 'add') {
|
|
200
203
|
const type = String(b.type ?? '').trim();
|
|
201
|
-
if (!NAME_RE.test(col)) problem = '
|
|
202
|
-
else if (cols.includes(col)) problem =
|
|
203
|
-
else if (!TYPE_RE.test(type)) problem = '
|
|
204
|
+
if (!NAME_RE.test(col)) problem = tx('ob.err.col_name');
|
|
205
|
+
else if (cols.includes(col)) problem = tx('ob.err.col_exists', { column: col });
|
|
206
|
+
else if (!TYPE_RE.test(type)) problem = tx('ob.err.type');
|
|
204
207
|
else sql = `alter table ${table.q} add column ${ident(col)} ${type}${b.not_null === 'true' ? ' not null' : ''};`;
|
|
205
208
|
} else if (b.action === 'rename') {
|
|
206
209
|
const to = String(b.new_name ?? '').trim();
|
|
207
|
-
if (!cols.includes(col)) problem = '
|
|
208
|
-
else if (!NAME_RE.test(to) || cols.includes(to)) problem = '
|
|
210
|
+
if (!cols.includes(col)) problem = tx('ob.err.choose_col');
|
|
211
|
+
else if (!NAME_RE.test(to) || cols.includes(to)) problem = tx('ob.err.new_name');
|
|
209
212
|
else sql = `alter table ${table.q} rename column ${ident(col)} to ${ident(to)};`;
|
|
210
213
|
} else if (b.action === 'modify') {
|
|
211
214
|
// (sprint 89, SQL-4) type, nulls and default in one ALTER TABLE; a default is one expression, never a statement
|
|
@@ -213,28 +216,28 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
213
216
|
const expr = String(b.default ?? '').trim();
|
|
214
217
|
const parts: string[] = [];
|
|
215
218
|
const c = ident(col);
|
|
216
|
-
if (!cols.includes(col)) problem = '
|
|
217
|
-
else if (type && !TYPE_RE.test(type)) problem = '
|
|
218
|
-
else if (b.default_action === 'set' && (!expr || expr.length > 200 || /;|--|\/\*/.test(expr))) problem = '
|
|
219
|
+
if (!cols.includes(col)) problem = tx('ob.err.choose_col');
|
|
220
|
+
else if (type && !TYPE_RE.test(type)) problem = tx('ob.err.type');
|
|
221
|
+
else if (b.default_action === 'set' && (!expr || expr.length > 200 || /;|--|\/\*/.test(expr))) problem = tx('ob.err.default');
|
|
219
222
|
else {
|
|
220
223
|
if (type) parts.push(`alter column ${c} type ${type} using ${c}::${type}`);
|
|
221
224
|
if (b.nullable === 'not_null') parts.push(`alter column ${c} set not null`);
|
|
222
225
|
else if (b.nullable === 'null') parts.push(`alter column ${c} drop not null`);
|
|
223
226
|
if (b.default_action === 'set') parts.push(`alter column ${c} set default ${expr}`);
|
|
224
227
|
else if (b.default_action === 'drop') parts.push(`alter column ${c} drop default`);
|
|
225
|
-
if (!parts.length) problem = '
|
|
228
|
+
if (!parts.length) problem = tx('ob.err.nothing');
|
|
226
229
|
else sql = `alter table ${table.q}\n ${parts.join(',\n ')};`;
|
|
227
230
|
}
|
|
228
231
|
} else if (b.action === 'drop') {
|
|
229
|
-
if (!cols.includes(col)) problem = '
|
|
232
|
+
if (!cols.includes(col)) problem = tx('ob.err.choose_col');
|
|
230
233
|
else sql = `alter table ${table.q} drop column ${ident(col)};`;
|
|
231
|
-
} else problem = '
|
|
234
|
+
} else problem = tx('ob.err.action');
|
|
232
235
|
if (!sql) {
|
|
233
236
|
flash(s, problem, 'error');
|
|
234
237
|
return back(reply, s, backTo);
|
|
235
238
|
}
|
|
236
239
|
s.state.__SQLDRAFT = sql;
|
|
237
|
-
flash(s, '
|
|
240
|
+
flash(s, tx('ob.review'));
|
|
238
241
|
return back(reply, s, `${BASE}/sql/worksheet`);
|
|
239
242
|
});
|
|
240
243
|
}
|
|
@@ -270,8 +273,9 @@ export async function listObjects(db: Awaited<ReturnType<typeof workshopDb>>, sc
|
|
|
270
273
|
return system ? rows : rows.filter((o) => !o.ext);
|
|
271
274
|
}
|
|
272
275
|
|
|
273
|
-
export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s:
|
|
274
|
-
const
|
|
276
|
+
export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s: Pick<Session, 'csrf_token' | 'state'>, schema: string, o: Obj, tabAsked: string, page: number, link: (x: Record<string, string>) => string): Promise<Raw> {
|
|
277
|
+
const tx = tr(s);
|
|
278
|
+
const table = (rows: Record<string, unknown>[], empty = tx('ob.none')) =>
|
|
275
279
|
rows.length
|
|
276
280
|
? html`<div class="table-wrap"><table class="report"><thead><tr>${Object.keys(rows[0]).map((k) => html`<th scope="col">${k}</th>`)}</tr></thead>
|
|
277
281
|
<tbody>${rows.map((r) => html`<tr>${Object.values(r).map((v) => html`<td>${v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v)}</td>`)}</tr>`)}</tbody></table></div>`
|
|
@@ -279,22 +283,22 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
279
283
|
const ddlOf = async (match: (x: { kind: string; name: string; table?: string }) => boolean) => {
|
|
280
284
|
const objects = await readSchema(db.pool as pg.Pool, schema).catch(() => []);
|
|
281
285
|
const text = objects.filter(match).map((x) => x.ddl).join('\n\n');
|
|
282
|
-
return text ? html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="
|
|
286
|
+
return text ? html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="${tx('ob.ddl_of', { name: o.name })}">${text}</pre>` : html`<p class="muted">${tx('ob.no_ddl')}</p>`;
|
|
283
287
|
};
|
|
284
288
|
const relname = o.ref.includes('.') ? o.ref.slice(o.ref.lastIndexOf('.') + 1).replace(/^"|"$/g, '') : o.ref.replace(/^"|"$/g, '');
|
|
285
289
|
|
|
286
290
|
if (o.type === 'function' || o.type === 'procedure') {
|
|
287
291
|
const src = await db.one<{ def: string }>('select pg_get_functiondef($1::regprocedure) as def', [o.ref]).catch(() => undefined);
|
|
288
|
-
return region(o.name, src ? html`<pre class="source" data-code-view="plpgsql" tabindex="0" aria-label="
|
|
292
|
+
return region(o.name, src ? html`<pre class="source" data-code-view="plpgsql" tabindex="0" aria-label="${tx('ob.source_of', { name: o.name })}">${src.def}</pre>` : html`<p class="muted">${tx('ob.no_source')}</p>`);
|
|
289
293
|
}
|
|
290
294
|
if (o.type === 'trigger') {
|
|
291
295
|
const [rel, name] = o.ref.split('|');
|
|
292
296
|
const t = await db.one<{ def: string }>('select pg_get_triggerdef(oid, true) as def from pg_trigger where tgrelid = $1::regclass and tgname = $2', [rel, name]);
|
|
293
|
-
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="
|
|
297
|
+
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="${tx('ob.definition_of', { name: o.name })}">${t?.def ?? ''}</pre>`);
|
|
294
298
|
}
|
|
295
299
|
if (o.type === 'index') {
|
|
296
300
|
const i = await db.one<{ def: string; size: string }>('select pg_get_indexdef($1::regclass) as def, pg_size_pretty(pg_relation_size($1::regclass)) as size', [o.ref]);
|
|
297
|
-
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="
|
|
301
|
+
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="${tx('ob.definition_of', { name: o.name })}">${i?.def ?? ''};</pre><p class="muted">${tx('ob.size', { size: i?.size ?? '' })}</p>`);
|
|
298
302
|
}
|
|
299
303
|
if (o.type === 'sequence') {
|
|
300
304
|
const q = await db.one<Record<string, unknown>>(
|
|
@@ -313,11 +317,11 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
313
317
|
const tab = tabs.some(([k]) => k === tabAsked) ? tabAsked : 'columns';
|
|
314
318
|
// (sprint 89, SQL-4) the tabs on one row: the main ones and the current one, the rest under More
|
|
315
319
|
const MAIN = ['columns', 'data', 'indexes', 'constraints', 'ddl'];
|
|
316
|
-
const tabLink = ([k
|
|
320
|
+
const tabLink = ([k]: readonly [string, string]) => html`<a href="${link({ k: o.type, o: o.ref, tab: k })}"${k === tab ? raw(' aria-current="page"') : ''}>${tx(`ob.tab.${k as TabKey}`)}</a>`;
|
|
317
321
|
const shownTabs = tabs.filter(([k]) => MAIN.includes(k) || k === tab);
|
|
318
322
|
const moreTabs = tabs.filter(([k]) => !shownTabs.some(([x]) => x === k));
|
|
319
323
|
const nav = html`<nav class="settings-tabs ob-tabs" aria-label="${o.name}">${shownTabs.map(tabLink)}${moreTabs.length
|
|
320
|
-
? html`<details class="menu ob-more"><summary
|
|
324
|
+
? html`<details class="menu ob-more"><summary>${tx('ob.more')} ${icon('chevron-down')}</summary><div class="menu-panel"><div class="menu-section menu-links">${moreTabs.map(tabLink)}</div></div></details>`
|
|
321
325
|
: ''}</nav>`;
|
|
322
326
|
let body: Raw;
|
|
323
327
|
switch (tab) {
|
|
@@ -333,30 +337,30 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
333
337
|
).rows;
|
|
334
338
|
const names = cols.map((c) => String(c.column));
|
|
335
339
|
const hidden = html`${raw(`<input type="hidden" name="__csrf" value="${s.csrf_token}">`)}<input type="hidden" name="table" value="${o.ref}"><input type="hidden" name="schema" value="${schema}">`;
|
|
336
|
-
const pick = (id: string) => html`<label class="label" for="${id}"
|
|
340
|
+
const pick = (id: string) => html`<label class="label" for="${id}">${tx('ob.column')}</label><select id="${id}" name="column">${names.map((n) => html`<option>${n}</option>`)}</select>`;
|
|
337
341
|
// (sprint 89, SQL-4; APEX: the Columns toolbar) Add, Modify, Rename and Drop above the columns, each a small form
|
|
338
342
|
body = html`${isTable
|
|
339
|
-
? html`<div class="ob-toolbar" role="toolbar" aria-label="
|
|
340
|
-
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('plus')}
|
|
343
|
+
? html`<div class="ob-toolbar" role="toolbar" aria-label="${tx('ob.tab.columns')}">
|
|
344
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('plus')} ${tx('ob.add_col')}</summary><div class="menu-panel">
|
|
341
345
|
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="add">
|
|
342
|
-
<label class="label" for="ob-add-name"
|
|
343
|
-
<label class="label" for="ob-add-type"
|
|
344
|
-
<label class="check"><input type="checkbox" name="not_null" value="true"> not null</label><button class="btn btn-sm btn-hot"
|
|
345
|
-
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('edit')}
|
|
346
|
+
<label class="label" for="ob-add-name">${tx('ob.name')}</label><input id="ob-add-name" name="column" placeholder="name" required>
|
|
347
|
+
<label class="label" for="ob-add-type">${tx('ob.type')}</label><input id="ob-add-type" name="type" placeholder="text" required>
|
|
348
|
+
<label class="check"><input type="checkbox" name="not_null" value="true"> not null</label><button class="btn btn-sm btn-hot">${tx('ob.show_sql')}</button></form></div></details>
|
|
349
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('edit')} ${tx('ob.modify_col')}</summary><div class="menu-panel">
|
|
346
350
|
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="modify">
|
|
347
351
|
${pick('ob-mod-col')}
|
|
348
|
-
<label class="label" for="ob-mod-type"
|
|
349
|
-
<label class="label" for="ob-mod-null"
|
|
350
|
-
<label class="label" for="ob-mod-def"
|
|
351
|
-
<label class="sr-only" for="ob-mod-expr"
|
|
352
|
-
<button class="btn btn-sm btn-hot"
|
|
353
|
-
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('tag')}
|
|
352
|
+
<label class="label" for="ob-mod-type">${tx('ob.new_type')}</label><input id="ob-mod-type" name="type" placeholder="${tx('ob.the_same')}">
|
|
353
|
+
<label class="label" for="ob-mod-null">${tx('ob.nulls')}</label><select id="ob-mod-null" name="nullable"><option value="">${tx('ob.the_same')}</option><option value="null">${tx('ob.allowed')}</option><option value="not_null">not null</option></select>
|
|
354
|
+
<label class="label" for="ob-mod-def">${tx('ob.default')}</label><select id="ob-mod-def" name="default_action"><option value="">${tx('ob.the_same')}</option><option value="set">${tx('ob.set_to')}</option><option value="drop">${tx('ob.default_none')}</option></select>
|
|
355
|
+
<label class="sr-only" for="ob-mod-expr">${tx('ob.default_expr')}</label><input id="ob-mod-expr" name="default" placeholder="now(), 0, 'x'">
|
|
356
|
+
<button class="btn btn-sm btn-hot">${tx('ob.show_sql')}</button></form></div></details>
|
|
357
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('tag')} ${tx('ob.rename_col')}</summary><div class="menu-panel">
|
|
354
358
|
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="rename">
|
|
355
|
-
${pick('ob-ren-col')}<label class="label" for="ob-ren-to"
|
|
356
|
-
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('trash')}
|
|
359
|
+
${pick('ob-ren-col')}<label class="label" for="ob-ren-to">${tx('ob.new_name')}</label><input id="ob-ren-to" name="new_name" placeholder="${tx('ob.new_name_ph')}" required><button class="btn btn-sm btn-hot">${tx('ob.show_sql')}</button></form></div></details>
|
|
360
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('trash')} ${tx('ob.drop_col')}</summary><div class="menu-panel">
|
|
357
361
|
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="drop">
|
|
358
|
-
${pick('ob-drop-col')}<button class="btn btn-sm btn-danger"
|
|
359
|
-
<span class="muted ob-toolbar-note"
|
|
362
|
+
${pick('ob-drop-col')}<button class="btn btn-sm btn-danger">${tx('ob.show_sql')}</button></form></div></details>
|
|
363
|
+
<span class="muted ob-toolbar-note">${tx('ob.toolbar_note')}</span>
|
|
360
364
|
</div>`
|
|
361
365
|
: ''}${table(cols)}`;
|
|
362
366
|
break;
|
|
@@ -369,9 +373,9 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
369
373
|
const more = data.rows.length > PAGE_SIZE;
|
|
370
374
|
const rows = data.rows.slice(0, PAGE_SIZE).map((r: unknown[]) => Object.fromEntries(data.fields.map((f, i) => [f.name, r[i]])));
|
|
371
375
|
const p = offset / PAGE_SIZE + 1;
|
|
372
|
-
body = html`${table(rows, '
|
|
373
|
-
<span
|
|
374
|
-
<p class="muted"
|
|
376
|
+
body = html`${table(rows, tx('ob.no_rows'))}<nav class="pager" aria-label="${tx('ob.pages')}">${p > 1 ? html`<a href="${link({ k: o.type, o: o.ref, tab: 'data', p: String(p - 1) })}">${tx('ob.previous')}</a>` : ''}
|
|
377
|
+
<span>${tx('ob.rows_range', { from: rows.length ? offset + 1 : 0, to: offset + rows.length })}</span>${more ? html`<a href="${link({ k: o.type, o: o.ref, tab: 'data', p: String(p + 1) })}">${tx('ob.next')}</a>` : ''}</nav>
|
|
378
|
+
<p class="muted">${tx('ob.change_rows')}</p>`;
|
|
375
379
|
}
|
|
376
380
|
break;
|
|
377
381
|
}
|
|
@@ -392,25 +396,25 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
392
396
|
break;
|
|
393
397
|
case 'rls': {
|
|
394
398
|
const on = await db.one<{ rls: boolean; force: boolean }>('select relrowsecurity as rls, relforcerowsecurity as force from pg_class where oid = $1::regclass', [o.ref]);
|
|
395
|
-
body = html`<p>${on?.rls ? html`<b
|
|
396
|
-
${table((await db.query(`select policyname as policy, permissive, cmd, roles::text, qual as "using", with_check from pg_policies where schemaname = $1 and tablename = $2 order by policyname`, [schema, relname])).rows, '
|
|
399
|
+
body = html`<p>${on?.rls ? html`<b>${tx('ob.rls_on')}</b>${on.force ? ` ${tx('ob.rls_forced')}` : ''}.` : tx('ob.rls_off')}</p>
|
|
400
|
+
${table((await db.query(`select policyname as policy, permissive, cmd, roles::text, qual as "using", with_check from pg_policies where schemaname = $1 and tablename = $2 order by policyname`, [schema, relname])).rows, tx('ob.no_policies'))}`;
|
|
397
401
|
break;
|
|
398
402
|
}
|
|
399
403
|
case 'dependencies':
|
|
400
|
-
body = html`<h3
|
|
404
|
+
body = html`<h3>${tx('ob.used_by')}</h3>${table((await db.query(
|
|
401
405
|
`select distinct dc.oid::regclass::text as object, case dc.relkind when 'v' then 'view' when 'm' then 'materialized view' else 'table' end as type
|
|
402
406
|
from pg_depend d join pg_rewrite r on r.oid = d.objid join pg_class dc on dc.oid = r.ev_class
|
|
403
407
|
where d.classid = 'pg_rewrite'::regclass and d.refobjid = $1::regclass and dc.oid <> $1::regclass
|
|
404
408
|
union
|
|
405
409
|
select conrelid::regclass::text, 'foreign key ' || conname from pg_constraint where confrelid = $1::regclass and contype = 'f' and conrelid <> $1::regclass
|
|
406
|
-
order by 1`, [o.ref])).rows, '
|
|
407
|
-
<h3
|
|
410
|
+
order by 1`, [o.ref])).rows, tx('ob.nothing_refers'))}
|
|
411
|
+
<h3>${tx('ob.uses')}</h3>${table((await db.query(
|
|
408
412
|
`select distinct d.refobjid::regclass::text as object, 'table or view' as type
|
|
409
413
|
from pg_rewrite r join pg_depend d on d.objid = r.oid and d.classid = 'pg_rewrite'::regclass and d.refclassid = 'pg_class'::regclass
|
|
410
414
|
where r.ev_class = $1::regclass and d.refobjid <> $1::regclass
|
|
411
415
|
union
|
|
412
416
|
select confrelid::regclass::text, 'foreign key ' || conname from pg_constraint where conrelid = $1::regclass and contype = 'f' and confrelid <> $1::regclass
|
|
413
|
-
order by 1`, [o.ref])).rows, '
|
|
417
|
+
order by 1`, [o.ref])).rows, tx('ob.refers_nothing'))}`;
|
|
414
418
|
break;
|
|
415
419
|
case 'statistics':
|
|
416
420
|
body = table([(await db.one(
|
|
@@ -422,5 +426,5 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
422
426
|
body = await ddlOf((x) => (x.kind === 'table' || x.kind === 'view' || x.kind === 'materialized view') ? x.name === relname : x.table === relname);
|
|
423
427
|
}
|
|
424
428
|
return html`<h2 class="ob-title">${icon(OB_TYPES.find(([t]) => t === o.type)![2])} ${o.ref}${o.rls ? html` <span class="tag">RLS</span>` : ''}${o.type === 'table'
|
|
425
|
-
? html` <a class="btn btn-sm" href="${BASE}/sql/objects/table?${new URLSearchParams({ schema, o: o.ref })}">${icon('edit')}
|
|
429
|
+
? html` <a class="btn btn-sm" href="${BASE}/sql/objects/table?${new URLSearchParams({ schema, o: o.ref })}">${icon('edit')} ${tx('ob.edit_table')}</a> <a class="btn btn-sm" href="${BASE}/sql/objects/security?${new URLSearchParams({ o: o.ref })}">${icon('shield')} ${tx('ob.security')}</a>` : ''}</h2>${nav}${region(tx(`ob.tab.${tab as TabKey}`), body)}`;
|
|
426
430
|
}
|