pgkiln 0.81.0 → 0.83.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/db/migrations/147_condition_types.sql +80 -0
- package/db/migrations/148_validation_types.sql +44 -0
- package/db/migrations/149_dynamic_action_events.sql +45 -0
- package/db/migrations/150_process_types.sql +23 -0
- package/db/migrations/151_item_types.sql +16 -0
- package/db/migrations/152_workflow_diagram_region.sql +10 -0
- package/db/migrations/153_component_events.sql +21 -0
- package/docs/apex-feature-parity.md +11 -9
- package/docs/guide/02-concepts.md +3 -3
- package/docs/guide/03-builder.md +8 -1
- package/docs/guide/04-pages-and-regions.md +19 -4
- package/docs/guide/05-items.md +5 -0
- package/docs/guide/06-processing.md +104 -17
- package/docs/guide/07-dynamic-actions.md +43 -1
- package/docs/guide/11-from-apex.md +8 -0
- package/docs/guide/12-development.md +9 -1
- package/examples/hr/hr_79_condition_types.sql +17 -0
- package/package.json +1 -1
- package/public/app.css +14 -0
- package/public/app.js +242 -13
- package/public/builder.css +25 -2
- package/public/data-modeler.js +8 -5
- package/public/sql-monitor.js +5 -2
- package/src/appfiles.ts +4 -4
- package/src/builder/advisor.ts +43 -7
- package/src/builder/ai-builder.ts +50 -46
- package/src/builder/api.ts +49 -49
- package/src/builder/autorest.ts +92 -87
- package/src/builder/changelog.ts +39 -37
- package/src/builder/code-editor.ts +14 -10
- package/src/builder/components.ts +54 -23
- package/src/builder/conditions.ts +64 -0
- package/src/builder/data-modeler.ts +89 -84
- package/src/builder/data-security.ts +39 -37
- package/src/builder/dataload.ts +69 -65
- package/src/builder/datapump.ts +108 -97
- package/src/builder/ddl-tools.ts +38 -34
- package/src/builder/designer.ts +9 -1
- package/src/builder/document-view.ts +11 -11
- package/src/builder/i18n/de.ts +1662 -0
- package/src/builder/i18n/en.ts +1662 -0
- package/src/builder/i18n/es.ts +1662 -0
- package/src/builder/i18n/fr.ts +1662 -0
- package/src/builder/i18n/it.ts +1662 -0
- package/src/builder/i18n/ja.ts +1662 -0
- package/src/builder/i18n/ko.ts +1662 -0
- package/src/builder/i18n/nl.ts +1662 -0
- package/src/builder/i18n/pt.ts +1662 -0
- package/src/builder/i18n/zh-cn.ts +1662 -0
- package/src/builder/i18n/zh-tw.ts +1662 -0
- package/src/builder/i18n.ts +18 -0
- package/src/builder/object-actions.ts +18 -16
- package/src/builder/object-browser.ts +2 -2
- package/src/builder/querybuilder.ts +42 -36
- package/src/builder/quicksql.ts +16 -18
- package/src/builder/region-help.ts +3 -1
- package/src/builder/region-settings.ts +225 -198
- package/src/builder/report-settings.ts +64 -60
- package/src/builder/rest-modules.ts +145 -143
- package/src/builder/sampledata.ts +59 -52
- package/src/builder/scheduling.ts +180 -159
- package/src/builder/scripts.ts +71 -65
- package/src/builder/sql-charts.ts +36 -30
- package/src/builder/sql-monitor.ts +95 -89
- package/src/builder/sql-utilities.ts +47 -40
- package/src/builder/top-sql.ts +21 -17
- package/src/builder/unload.ts +48 -44
- package/src/builder/worksheet.ts +3 -3
- package/src/cron-schedule.ts +47 -17
- package/src/i18n/ar.ts +5 -0
- package/src/i18n/cs.ts +5 -0
- package/src/i18n/da.ts +5 -0
- package/src/i18n/de.ts +5 -0
- package/src/i18n/el.ts +5 -0
- package/src/i18n/es.ts +5 -0
- package/src/i18n/fi.ts +5 -0
- package/src/i18n/fr.ts +5 -0
- package/src/i18n/he.ts +5 -0
- package/src/i18n/it.ts +5 -0
- package/src/i18n/ja.ts +5 -0
- package/src/i18n/ko.ts +5 -0
- package/src/i18n/nb.ts +5 -0
- package/src/i18n/pl.ts +5 -0
- package/src/i18n/pt.ts +5 -0
- package/src/i18n/ru.ts +5 -0
- package/src/i18n/sv.ts +5 -0
- package/src/i18n/tr.ts +5 -0
- package/src/i18n/uk.ts +5 -0
- package/src/i18n/zh.ts +5 -0
- package/src/i18n.ts +10 -0
- package/src/metadata.ts +74 -9
- package/src/runtime/authz.ts +9 -5
- package/src/runtime/calendar.ts +3 -2
- package/src/runtime/context.ts +6 -0
- package/src/runtime/da-styles.ts +37 -0
- package/src/runtime/documents.ts +3 -2
- package/src/runtime/engine.ts +95 -13
- package/src/runtime/grid.ts +1 -0
- package/src/runtime/items.ts +33 -0
- package/src/runtime/logic.ts +79 -8
- package/src/runtime/maps.ts +56 -7
- package/src/runtime/process-types.ts +139 -0
- package/src/runtime/processes.ts +2 -0
- package/src/runtime/regions.ts +4 -0
- package/src/runtime/render.ts +19 -3
- package/src/runtime/routes.ts +6 -2
- package/src/runtime/tree.ts +4 -3
- package/src/runtime/workflow-diagram.ts +45 -0
- package/src/workflow.ts +10 -2
|
@@ -7,6 +7,7 @@ import { autoLayout, cleanObjects, diagramSvg, lines, markers, matchNames, model
|
|
|
7
7
|
import type { Session } from '../session.ts';
|
|
8
8
|
import { back, BASE, bicon, csrf, developer, flash, region, send, shell, workshopTabs, type Req } from './ui.ts';
|
|
9
9
|
import { currentWorkspace, sessionSchema, usableSchemas, workshopDb } from './workspaces.ts';
|
|
10
|
+
import { tr, type BuilderKey, type BuilderTranslate } from './i18n.ts';
|
|
10
11
|
|
|
11
12
|
// (sprint 98, Database Actions DA-10: Data Modeler) SQL Workshop → Data Modeler: named relational diagrams of a
|
|
12
13
|
// schema's tables and views, per workspace (meta.sql_diagram, migration 111). A diagram keeps only the objects'
|
|
@@ -69,28 +70,29 @@ async function listPage(req: Req, reply: FastifyReply) {
|
|
|
69
70
|
from meta.sql_diagram where workspace_id = $1 order by lower(name)`, [currentWorkspace(s)]),
|
|
70
71
|
]);
|
|
71
72
|
const pick = current ?? (schemas.includes('public') ? 'public' : schemas[0]);
|
|
73
|
+
const tx = tr(s);
|
|
72
74
|
const list = rows.rows.length
|
|
73
|
-
? html`<div class="table-wrap"><table class="report report-reflow" aria-label="
|
|
75
|
+
? html`<div class="table-wrap"><table class="report report-reflow" aria-label="${tx('dm.diagrams')}"><thead><tr><th>${tx('scr.name')}</th><th>${tx('sm.schema')}</th><th class="num">${tx('ddl.objects')}</th><th>${tx('scr.col.updated')}</th><th>${tx('scr.col.by')}</th><th><span class="sr-only">${tx('sm.col.actions')}</span></th></tr></thead>
|
|
74
76
|
<tbody>${rows.rows.map((d) => html`<tr>
|
|
75
|
-
<td data-label="
|
|
76
|
-
<td data-label="
|
|
77
|
-
<td data-label="
|
|
78
|
-
<td data-label="
|
|
79
|
-
<td data-label="
|
|
80
|
-
<td data-label="
|
|
81
|
-
: html`<p class="muted"
|
|
77
|
+
<td data-label="${tx('scr.name')}" class="u-break"><a href="${M}/${d.id}">${d.name}</a></td>
|
|
78
|
+
<td data-label="${tx('sm.schema')}" class="u-break">${d.schema_name}</td>
|
|
79
|
+
<td data-label="${tx('ddl.objects')}" class="num">${d.n}</td>
|
|
80
|
+
<td data-label="${tx('scr.col.updated')}">${when(d.updated_at)}</td>
|
|
81
|
+
<td data-label="${tx('scr.col.by')}" class="u-break">${d.updated_by ?? d.created_by}</td>
|
|
82
|
+
<td data-label="${tx('sm.col.actions')}"><form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-sm btn-danger" data-confirm="${tx('dm.delete_confirm', { name: d.name })}">${icon('trash')} ${tx('scr.delete')}</button></form></td></tr>`)}</tbody></table></div>`
|
|
83
|
+
: html`<p class="muted">${tx('dm.none_yet')}</p>`;
|
|
82
84
|
const create = html`<form method="post" action="${M}" class="dm-create">${csrf(s)}
|
|
83
85
|
<div class="form-grid">
|
|
84
|
-
<div class="field"><label class="label" for="f_name"
|
|
85
|
-
<div class="field"><label class="label" for="f_schema"
|
|
86
|
-
<div class="field"><label class="label" for="f_objects"
|
|
87
|
-
<small class="help"
|
|
86
|
+
<div class="field"><label class="label" for="f_name">${tx('scr.name')}</label><input id="f_name" name="name" required maxlength="120" placeholder="${tx('dm.name_placeholder')}"></div>
|
|
87
|
+
<div class="field"><label class="label" for="f_schema">${tx('sm.schema')}</label><select id="f_schema" name="schema">${schemas.map((n) => html`<option${n === pick ? raw(' selected') : ''}>${n}</option>`)}</select></div>
|
|
88
|
+
<div class="field"><label class="label" for="f_objects">${tx('ddl.objects')}</label><input id="f_objects" name="objects" maxlength="2000" placeholder="${tx('dm.objects_placeholder')}">
|
|
89
|
+
<small class="help">${tx.code('dm.objects_help')}</small></div>
|
|
88
90
|
</div>
|
|
89
|
-
<div class="buttons"><button class="btn btn-hot">${icon('plus')}
|
|
90
|
-
const main = html`<h1 class="u-mb1"
|
|
91
|
-
<p class="muted u-mt0"
|
|
92
|
-
${region('
|
|
93
|
-
return send(reply, s, shell(s, '
|
|
91
|
+
<div class="buttons"><button class="btn btn-hot">${icon('plus')} ${tx('dm.create')}</button></div></form>`;
|
|
92
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('modeler', s)}
|
|
93
|
+
<p class="muted u-mt0">${tx('dm.intro')}</p>
|
|
94
|
+
${region(tx('dm.diagrams'), list)}<div class="u-spacer"></div>${region(tx('dm.create'), create)}`;
|
|
95
|
+
return send(reply, s, shell(s, tx('sql.tool.modeler'), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.modeler')]], main, 'sql'));
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
/** One diagram: the toolbar, the SVG, its objects, Add objects. */
|
|
@@ -100,10 +102,11 @@ async function diagramPage(req: Req, reply: FastifyReply) {
|
|
|
100
102
|
const d = await loadDiagram(s, req.params.id);
|
|
101
103
|
if (!d) return notFound(reply);
|
|
102
104
|
const m = await model(s, d);
|
|
103
|
-
const
|
|
105
|
+
const tx = tr(s);
|
|
106
|
+
const crumbs: [string, string?][] = [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.modeler'), M], [d.name]];
|
|
104
107
|
if (!m) {
|
|
105
|
-
const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler', s)}<div class="alert alert-error" role="alert"
|
|
106
|
-
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="
|
|
108
|
+
const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler', s)}<div class="alert alert-error" role="alert">${tx('dm.schema_gone', { schema: d.schema_name })}</div>
|
|
109
|
+
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="${tx('dm.delete_confirm_short', { name: d.name })}">${icon('trash')} ${tx('dm.delete_diagram')}</button></form>`;
|
|
107
110
|
return send(reply, s, shell(s, d.name, crumbs, main, 'sql'));
|
|
108
111
|
}
|
|
109
112
|
const { tables, placed, missing } = m;
|
|
@@ -116,51 +119,51 @@ async function diagramPage(req: Req, reply: FastifyReply) {
|
|
|
116
119
|
const fks = lines(d.schema_name, tables);
|
|
117
120
|
const jsBtn = (act: string, ic: Raw | '', label: string, extra = '') =>
|
|
118
121
|
html`<button type="button" class="btn btn-sm" data-dm="${act}" hidden title="${label}${extra ? ` (${extra})` : ''}" aria-label="${label}">${ic}<span class="dm-btn-label">${label}</span></button>`;
|
|
119
|
-
const toolbar = html`<div class="dm-toolbar" role="toolbar" aria-label="
|
|
122
|
+
const toolbar = html`<div class="dm-toolbar" role="toolbar" aria-label="${tx('dm.diagram')}">
|
|
120
123
|
<form method="post" action="${M}/${d.id}/layout" class="dm-save-form">${csrf(s)}<input type="hidden" name="positions" value="">
|
|
121
|
-
${jsBtn('save', icon('save'), '
|
|
122
|
-
${jsBtn('undo', icon('undo'), '
|
|
123
|
-
${jsBtn('zoom-out', bicon('zoom-out'), '
|
|
124
|
-
<form method="post" action="${M}/${d.id}/layout">${csrf(s)}<input type="hidden" name="layout" value="auto"><button class="btn btn-sm" data-dm="auto" title="
|
|
125
|
-
${jsBtn('minimap', icon('map'), '
|
|
126
|
-
<a class="btn btn-sm" href="${M}/${d.id}/svg" data-dm="svg" title="
|
|
127
|
-
${jsBtn('png', icon('image'), 'PNG')}${jsBtn('print', icon('printer'), '
|
|
128
|
-
<a class="btn btn-sm" href="${M}/${d.id}/ddl">${icon('code')}<span class="dm-btn-label"
|
|
129
|
-
<a class="btn btn-sm" href="${M}/${d.id}/dictionary">${icon('book')}<span class="dm-btn-label"
|
|
130
|
-
<a class="btn btn-sm btn-hot" href="${M}/${d.id}#dm-add">${icon('plus')}<span class="dm-btn-label"
|
|
124
|
+
${jsBtn('save', icon('save'), tx('scr.save'), 'Ctrl+S')}<span class="dm-status muted small" role="status" aria-live="polite"></span></form>
|
|
125
|
+
${jsBtn('undo', icon('undo'), tx('dm.undo'), 'Ctrl+Z')}${jsBtn('redo', icon('redo'), tx('dm.redo'), 'Ctrl+Y')}
|
|
126
|
+
${jsBtn('zoom-out', bicon('zoom-out'), tx('dm.zoom_out'))}${jsBtn('zoom-in', bicon('zoom-in'), tx('dm.zoom_in'))}${jsBtn('fit', bicon('expand'), tx('dm.fit'))}
|
|
127
|
+
<form method="post" action="${M}/${d.id}/layout">${csrf(s)}<input type="hidden" name="layout" value="auto"><button class="btn btn-sm" data-dm="auto" title="${tx('dm.auto')}" aria-label="${tx('dm.auto')}">${icon('grid')}<span class="dm-btn-label">${tx('dm.auto')}</span></button></form>
|
|
128
|
+
${jsBtn('minimap', icon('map'), tx('dm.minimap'))}
|
|
129
|
+
<a class="btn btn-sm" href="${M}/${d.id}/svg" data-dm="svg" title="${tx('dm.export_svg')}" aria-label="${tx('dm.export_svg')}">${icon('download')}<span class="dm-btn-label">SVG</span></a>
|
|
130
|
+
${jsBtn('png', icon('image'), 'PNG')}${jsBtn('print', icon('printer'), tx('dm.print'))}
|
|
131
|
+
<a class="btn btn-sm" href="${M}/${d.id}/ddl">${icon('code')}<span class="dm-btn-label">${tx('sql.tool.ddl')}</span></a>
|
|
132
|
+
<a class="btn btn-sm" href="${M}/${d.id}/dictionary">${icon('book')}<span class="dm-btn-label">${tx('dm.dictionary')}</span></a>
|
|
133
|
+
<a class="btn btn-sm btn-hot" href="${M}/${d.id}#dm-add">${icon('plus')}<span class="dm-btn-label">${tx('dm.add_objects')}</span></a>
|
|
131
134
|
</div>`;
|
|
132
135
|
const objectList = tables.length
|
|
133
|
-
? html`<div class="table-wrap"><table class="report report-reflow" aria-label="
|
|
136
|
+
? html`<div class="table-wrap"><table class="report report-reflow" aria-label="${tx('dm.objects_in')}"><thead><tr><th>${tx('sm.col.object')}</th><th>${tx('dm.kind')}</th><th class="num">${tx('ul.columns')}</th><th>${tx('dm.references')}</th><th><span class="sr-only">${tx('sm.col.actions')}</span></th></tr></thead>
|
|
134
137
|
<tbody>${tables.map((t) => html`<tr>
|
|
135
|
-
<td data-label="
|
|
136
|
-
<td data-label="
|
|
137
|
-
<td data-label="
|
|
138
|
-
<td data-label="
|
|
139
|
-
<td data-label="
|
|
140
|
-
: html`<p class="muted"
|
|
138
|
+
<td data-label="${tx('sm.col.object')}" class="u-break"><a href="${objectHref(d.schema_name, t)}" title="${t.kind === 'table' ? tx('cl.from.table') : tx('sql.tool.objects')}">${t.name}</a>${t.comment ? html`<br><span class="muted small">${t.comment}</span>` : ''}</td>
|
|
139
|
+
<td data-label="${tx('dm.kind')}">${t.kind}</td>
|
|
140
|
+
<td data-label="${tx('ul.columns')}" class="num">${t.columns.length}</td>
|
|
141
|
+
<td data-label="${tx('dm.references')}" class="u-break">${fks.filter((l) => l.from === t.name).map((l) => l.to).join(', ') || '-'}</td>
|
|
142
|
+
<td data-label="${tx('sm.col.actions')}"><form method="post" action="${M}/${d.id}/objects">${csrf(s)}<input type="hidden" name="remove" value="${t.name}"><button class="btn btn-sm" aria-label="${tx('dm.remove_label', { name: t.name })}">${icon('minus')} ${tx('dm.remove')}</button></form></td></tr>`)}</tbody></table></div>`
|
|
143
|
+
: html`<p class="muted">${tx('dm.no_objects')}</p>`;
|
|
141
144
|
const add = html`<form method="get" action="${M}/${d.id}#dm-add" class="dm-search" role="search">
|
|
142
|
-
<label class="label" for="f_q"
|
|
143
|
-
<div class="dm-search-row"><input id="f_q" name="q" type="search" value="${q}" placeholder="
|
|
145
|
+
<label class="label" for="f_q">${tx('dm.search_label', { schema: d.schema_name })}</label>
|
|
146
|
+
<div class="dm-search-row"><input id="f_q" name="q" type="search" value="${q}" placeholder="${tx('dm.search_placeholder')}" maxlength="200"><button class="btn">${icon('search')} ${tx('common.search')}</button></div></form>
|
|
144
147
|
${q
|
|
145
148
|
? found.length
|
|
146
|
-
? html`<form method="post" action="${M}/${d.id}/objects">${csrf(s)}<fieldset class="field"><legend class="label">${found.length
|
|
149
|
+
? html`<form method="post" action="${M}/${d.id}/objects">${csrf(s)}<fieldset class="field"><legend class="label">${found.length === 500 ? tx('dm.found_first') : tx('dm.found', { n: found.length })}</legend><div class="qb-pick">
|
|
147
150
|
${found.map((n) => html`<label class="check u-break"><input type="checkbox" name="t" value="${n}" checked> ${n}${kindOf.get(n) !== 'table' ? html` <span class="tag">${kindOf.get(n)}</span>` : ''}</label>`)}</div></fieldset>
|
|
148
|
-
<div class="buttons"><button class="btn btn-hot">${icon('plus')}
|
|
149
|
-
: html`<p class="muted"
|
|
151
|
+
<div class="buttons"><button class="btn btn-hot">${icon('plus')} ${tx('dm.add_to')}</button></div></form>`
|
|
152
|
+
: html`<p class="muted">${tx('dm.nothing_matches', { q })}</p>`
|
|
150
153
|
: ''}`;
|
|
151
|
-
const settings = html`<details class="dm-settings"><summary
|
|
152
|
-
<form method="post" action="${M}/${d.id}/rename" class="dm-search-row">${csrf(s)}<label class="sr-only" for="f_rename"
|
|
153
|
-
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="
|
|
154
|
-
const main = html`<h1 class="u-mb1"
|
|
155
|
-
<div class="dm-head"><h2 class="dm-name">${d.name}</h2><span class="muted small"
|
|
156
|
-
${missing.length ? html`<div class="alert alert-error" role="alert"
|
|
154
|
+
const settings = html`<details class="dm-settings"><summary>${tx('dm.settings')}</summary>
|
|
155
|
+
<form method="post" action="${M}/${d.id}/rename" class="dm-search-row">${csrf(s)}<label class="sr-only" for="f_rename">${tx('scr.name')}</label><input id="f_rename" name="name" value="${d.name}" maxlength="120" required><button class="btn">${icon('edit')} ${tx('cl.rename')}</button></form>
|
|
156
|
+
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="${tx('dm.delete_confirm', { name: d.name })}">${icon('trash')} ${tx('dm.delete_diagram')}</button></form></details>`;
|
|
157
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('modeler', s)}
|
|
158
|
+
<div class="dm-head"><h2 class="dm-name">${d.name}</h2><span class="muted small">${tx('dm.head', { schema: d.schema_name, n: tables.length, time: when(d.updated_at), user: d.updated_by ?? d.created_by })}</span></div>
|
|
159
|
+
${missing.length ? html`<div class="alert alert-error" role="alert">${tx('dm.missing', { schema: d.schema_name, names: missing.join(', ') })}</div>` : ''}
|
|
157
160
|
${toolbar}
|
|
158
161
|
<div class="dm-stage" data-dm-stage data-name="${d.name}">
|
|
159
|
-
<div class="dm-canvas" tabindex="0" aria-label="
|
|
162
|
+
<div class="dm-canvas" tabindex="0" aria-label="${tx('dm.canvas_label', { name: d.name })}">${svg}</div>
|
|
160
163
|
</div>
|
|
161
|
-
<p class="muted small dm-legend"
|
|
162
|
-
${region('
|
|
163
|
-
${region('
|
|
164
|
+
<p class="muted small dm-legend">${tx.rich('dm.legend', { star: html`<strong>*</strong>` })} ${tables.some((t) => t.kind === 'table') ? tx('dm.legend_opens_table') : tx('dm.legend_opens_object')}</p>
|
|
165
|
+
${region(tx('ddl.objects'), objectList, '', 'dm-objects')}<div class="u-spacer"></div>
|
|
166
|
+
${region(tx('dm.add_title'), add, '', 'dm-add')}<div class="u-spacer"></div>
|
|
164
167
|
${settings}`;
|
|
165
168
|
return send(reply, s, shell(s, d.name, crumbs, main, 'sql'));
|
|
166
169
|
}
|
|
@@ -188,23 +191,23 @@ export function diagramDdl(schema: string, names: Set<string>, objects: DdlObjec
|
|
|
188
191
|
return ddlScript(schema, keep).replace(/^-- DDL of schema .*$/m, `-- DDL of ${names.size} objects of schema ${schema} (a Data Modeler diagram)`);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
const CON: Record<string,
|
|
194
|
+
const CON: Record<string, BuilderKey> = { p: 'dm.con.p', u: 'dm.con.u', f: 'dm.con.f', c: 'dm.con.c' };
|
|
192
195
|
|
|
193
196
|
/** The data dictionary: tables, columns, comments, constraints. */
|
|
194
|
-
function dictionary(schema: string, d: Diagram, tables: ModelTable[]): Raw {
|
|
197
|
+
function dictionary(tx: BuilderTranslate, schema: string, d: Diagram, tables: ModelTable[]): Raw {
|
|
195
198
|
const fks = lines(schema, tables);
|
|
196
|
-
return html`<header class="dm-dict-head"><h1
|
|
197
|
-
<p
|
|
198
|
-
<nav aria-label="
|
|
199
|
+
return html`<header class="dm-dict-head"><h1>${tx('dm.dict_title', { name: d.name })}</h1>
|
|
200
|
+
<p>${tx('dm.dict_head', { schema, n: tables.length, time: when(new Date().toISOString()) })}</p></header>
|
|
201
|
+
<nav aria-label="${tx('ddl.objects')}"><ol class="dm-dict-toc">${tables.map((t, i) => html`<li><a href="#t${i}">${t.name}</a> <span class="muted">${t.kind}</span></li>`)}</ol></nav>
|
|
199
202
|
${tables.map((t, i) => html`<section class="dm-dict-table" id="t${i}">
|
|
200
203
|
<h2>${t.name} <span class="muted small">${t.kind}</span></h2>
|
|
201
204
|
${t.comment ? html`<p>${t.comment}</p>` : ''}
|
|
202
|
-
<div class="table-wrap"><table class="report" aria-label="
|
|
205
|
+
<div class="table-wrap"><table class="report" aria-label="${tx('dm.columns_of', { name: t.name })}"><thead><tr><th>#</th><th>${tx('dm.col.column')}</th><th>${tx('ch.type')}</th><th>${tx('dm.col.null')}</th><th>${tx('dm.col.default')}</th><th>${tx('dm.col.key')}</th><th>${tx('dm.col.comment')}</th></tr></thead>
|
|
203
206
|
<tbody>${t.columns.map((c, k) => html`<tr><td class="num">${k + 1}</td><td class="u-break"><code>${c.name}</code></td><td class="u-break">${c.type}</td><td>${c.notNull ? 'not null' : 'null'}</td>
|
|
204
207
|
<td class="u-break">${c.default ?? ''}</td><td>${markers(t, c.name)}</td><td class="u-break">${c.comment ?? ''}</td></tr>`)}</tbody></table></div>
|
|
205
|
-
${t.constraints.length ? html`<h3
|
|
206
|
-
<tbody>${t.constraints.map((k) => html`<tr><td class="u-break"><code>${k.name}</code></td><td>${CON[k.type]}</td><td class="u-break"><code>${k.def}</code></td></tr>`)}</tbody></table></div>` : ''}
|
|
207
|
-
${fks.some((l) => l.to === t.name) ? html`<h3
|
|
208
|
+
${t.constraints.length ? html`<h3>${tx('dm.constraints')}</h3><div class="table-wrap"><table class="report" aria-label="${tx('dm.constraints_of', { name: t.name })}"><thead><tr><th>${tx('scr.name')}</th><th>${tx('ch.type')}</th><th>${tx('dm.definition')}</th></tr></thead>
|
|
209
|
+
<tbody>${t.constraints.map((k) => html`<tr><td class="u-break"><code>${k.name}</code></td><td>${CON[k.type] ? tx(CON[k.type]) : k.type}</td><td class="u-break"><code>${k.def}</code></td></tr>`)}</tbody></table></div>` : ''}
|
|
210
|
+
${fks.some((l) => l.to === t.name) ? html`<h3>${tx('dm.referenced_by')}</h3><ul>${fks.filter((l) => l.to === t.name).map((l) => html`<li>${l.label}</li>`)}</ul>` : ''}
|
|
208
211
|
</section>`)}`;
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -226,7 +229,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
226
229
|
const db = await workshopDb(s);
|
|
227
230
|
if (!(await usableSchemas(db)).includes(schema)) return notFound(reply);
|
|
228
231
|
if (!name) {
|
|
229
|
-
flash(s, '
|
|
232
|
+
flash(s, tr(s)('dm.err.name'), 'error');
|
|
230
233
|
return back(reply, s, M);
|
|
231
234
|
}
|
|
232
235
|
const names = matchNames((await modelNames(db, schema)).map((o) => o.name), String(req.body?.objects ?? '').slice(0, 2000), { exact: true }).slice(0, 500);
|
|
@@ -235,11 +238,11 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
235
238
|
const row = await owner.one<{ id: string }>(
|
|
236
239
|
`insert into meta.sql_diagram (workspace_id, name, schema_name, objects, created_by) values ($1, $2, $3, $4, $5) returning id::text`,
|
|
237
240
|
[currentWorkspace(s), name, schema, JSON.stringify(autoLayout(schema, tables)), s.username]);
|
|
238
|
-
flash(s,
|
|
241
|
+
flash(s, tables.length ? tr(s)('dm.created_n', { name, n: tables.length }) : tr(s)('dm.created_empty', { name }));
|
|
239
242
|
return back(reply, s, `${M}/${row!.id}${tables.length ? '' : '#dm-add'}`);
|
|
240
243
|
} catch (e: any) {
|
|
241
244
|
if (e.code !== '23505') throw e;
|
|
242
|
-
flash(s,
|
|
245
|
+
flash(s, tr(s)('dm.err.exists', { name }), 'error');
|
|
243
246
|
return back(reply, s, M);
|
|
244
247
|
}
|
|
245
248
|
});
|
|
@@ -259,18 +262,18 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
259
262
|
const remove = typeof req.body?.remove === 'string' ? req.body.remove : '';
|
|
260
263
|
if (remove) {
|
|
261
264
|
await saveObjects(s, d.id, m.placed.filter((p) => p.name !== remove));
|
|
262
|
-
flash(s,
|
|
265
|
+
flash(s, tr(s)('dm.removed', { name: remove }));
|
|
263
266
|
return back(reply, s, `${M}/${d.id}`);
|
|
264
267
|
}
|
|
265
268
|
const existing = new Set((await modelNames(m.db, d.schema_name)).map((o) => o.name));
|
|
266
269
|
const adding = [...new Set(wanted)].filter((n) => existing.has(n) && !m.placed.some((p) => p.name === n)).slice(0, Math.max(0, 500 - m.placed.length));
|
|
267
270
|
if (!adding.length) {
|
|
268
|
-
flash(s, '
|
|
271
|
+
flash(s, tr(s)('dm.err.nothing'), 'error');
|
|
269
272
|
return back(reply, s, `${M}/${d.id}#dm-add`);
|
|
270
273
|
}
|
|
271
274
|
const added = await readModel(m.db, d.schema_name, adding);
|
|
272
275
|
await saveObjects(s, d.id, placeAll(d.schema_name, [...m.tables, ...added], m.placed));
|
|
273
|
-
flash(s,
|
|
276
|
+
flash(s, tr(s)(added.length === 1 ? 'dm.added_one' : 'dm.added', { n: added.length, names: added.map((t) => t.name).join(', ') }));
|
|
274
277
|
return back(reply, s, `${M}/${d.id}`);
|
|
275
278
|
});
|
|
276
279
|
|
|
@@ -298,7 +301,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
298
301
|
}
|
|
299
302
|
await saveObjects(s, d.id, next);
|
|
300
303
|
if (json) return reply.send({ ok: true, saved: next.length });
|
|
301
|
-
flash(s, req.body?.layout === 'auto' ? '
|
|
304
|
+
flash(s, req.body?.layout === 'auto' ? tr(s)('dm.laid_out') : tr(s)('dm.saved'));
|
|
302
305
|
return back(reply, s, `${M}/${d.id}`);
|
|
303
306
|
});
|
|
304
307
|
|
|
@@ -309,15 +312,15 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
309
312
|
if (!d) return notFound(reply);
|
|
310
313
|
const name = String(req.body?.name ?? '').trim().slice(0, 120);
|
|
311
314
|
if (!name) {
|
|
312
|
-
flash(s, '
|
|
315
|
+
flash(s, tr(s)('dm.err.name'), 'error');
|
|
313
316
|
return back(reply, s, `${M}/${d.id}`);
|
|
314
317
|
}
|
|
315
318
|
try {
|
|
316
319
|
await owner.query(`update meta.sql_diagram set name = $3, updated_by = $4, updated_at = now() where id = $1 and workspace_id = $2`, [d.id, currentWorkspace(s), name, s.username]);
|
|
317
|
-
flash(s,
|
|
320
|
+
flash(s, tr(s)('dm.renamed', { name }));
|
|
318
321
|
} catch (e: any) {
|
|
319
322
|
if (e.code !== '23505') throw e;
|
|
320
|
-
flash(s,
|
|
323
|
+
flash(s, tr(s)('dm.err.exists', { name }), 'error');
|
|
321
324
|
}
|
|
322
325
|
return back(reply, s, `${M}/${d.id}`);
|
|
323
326
|
});
|
|
@@ -328,7 +331,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
328
331
|
const d = await loadDiagram(s, req.params.id);
|
|
329
332
|
if (!d) return notFound(reply);
|
|
330
333
|
await owner.query('delete from meta.sql_diagram where id = $1 and workspace_id = $2', [d.id, currentWorkspace(s)]);
|
|
331
|
-
flash(s,
|
|
334
|
+
flash(s, tr(s)('dm.deleted', { name: d.name }));
|
|
332
335
|
return back(reply, s, M);
|
|
333
336
|
});
|
|
334
337
|
|
|
@@ -353,13 +356,14 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
353
356
|
const script = diagramDdl(d.schema_name, new Set(m.tables.map((t) => t.name)), await readSchema(m.db.pool, d.schema_name));
|
|
354
357
|
if (req.query.download === '1')
|
|
355
358
|
return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}.sql"`).type('application/sql; charset=utf-8').send(script);
|
|
356
|
-
const
|
|
357
|
-
|
|
359
|
+
const tx = tr(s);
|
|
360
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('modeler', s)}
|
|
361
|
+
${region(tx('ddl.of', { schema: d.name }), html`<p class="muted u-mt0">${tx('dm.ddl_intro', { n: m.tables.length, schema: d.schema_name })}</p>
|
|
358
362
|
<pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL">${script}</pre>
|
|
359
363
|
<form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${script}">
|
|
360
|
-
<div class="buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')}
|
|
361
|
-
<button class="btn btn-hot">${icon('play')}
|
|
362
|
-
return send(reply, s, shell(s,
|
|
364
|
+
<div class="buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')} ${tx('dm.back')}</a><a class="btn" href="${M}/${d.id}/ddl?download=1">${icon('download')} ${tx('scr.download')}</a>
|
|
365
|
+
<button class="btn btn-hot">${icon('play')} ${tx('dm.open_worksheet')}</button></div></form>`)}`;
|
|
366
|
+
return send(reply, s, shell(s, tx('ddl.of', { schema: d.name }), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.modeler'), M], [d.name, `${M}/${d.id}`], ['DDL']], main, 'sql'));
|
|
363
367
|
});
|
|
364
368
|
|
|
365
369
|
// the data dictionary report (download=1: a standalone HTML file)
|
|
@@ -369,14 +373,15 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
369
373
|
const d = await loadDiagram(s, req.params.id);
|
|
370
374
|
const m = d && (await model(s, d));
|
|
371
375
|
if (!d || !m) return notFound(reply);
|
|
372
|
-
const
|
|
376
|
+
const tx = tr(s);
|
|
377
|
+
const body = dictionary(tx, d.schema_name, d, m.tables);
|
|
373
378
|
if (req.query.download === '1')
|
|
374
379
|
return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}-dictionary.html"`).type('text/html; charset=utf-8')
|
|
375
|
-
.send(`<!doctype html><html lang="
|
|
376
|
-
const main = html`<div class="dm-dict-actions buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')}
|
|
377
|
-
<a class="btn" href="${M}/${d.id}/dictionary?download=1">${icon('download')}
|
|
378
|
-
<button type="button" class="btn" data-dm="print" hidden>${icon('printer')}
|
|
380
|
+
.send(`<!doctype html><html lang="${tx.lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(tx('dm.dict_title', { name: d.name }))}</title><style>${DICT_CSS}</style></head><body>${body}</body></html>`);
|
|
381
|
+
const main = html`<div class="dm-dict-actions buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')} ${tx('dm.back')}</a>
|
|
382
|
+
<a class="btn" href="${M}/${d.id}/dictionary?download=1">${icon('download')} ${tx('dm.download_html')}</a>
|
|
383
|
+
<button type="button" class="btn" data-dm="print" hidden>${icon('printer')} ${tx('dm.print_pdf')}</button></div>
|
|
379
384
|
<article class="dm-dict">${body}</article>`;
|
|
380
|
-
return send(reply, s, shell(s,
|
|
385
|
+
return send(reply, s, shell(s, tx('dm.dict_title', { name: d.name }), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.modeler'), M], [d.name, `${M}/${d.id}`], [tx('dm.dictionary')]], main, 'sql'));
|
|
381
386
|
});
|
|
382
387
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
2
|
-
import { html, raw, type Raw } from '../html.ts';
|
|
2
|
+
import { esc, html, raw, type Raw } from '../html.ts';
|
|
3
3
|
import { icon } from '../icons.ts';
|
|
4
4
|
import { owner } from '../db.ts';
|
|
5
5
|
import { clientIp, logActivity, type Session } from '../session.ts';
|
|
@@ -9,6 +9,7 @@ import { clearCompletions } from './code-editor.ts';
|
|
|
9
9
|
import { roleHints, splitRoles } from './users.ts';
|
|
10
10
|
import { currentWorkspace, inWorkspaceSql, usableSchemas, workshopDb } from './workspaces.ts';
|
|
11
11
|
import { joinStatements, recordChanges } from '../changelog.ts';
|
|
12
|
+
import { tr } from './i18n.ts';
|
|
12
13
|
import {
|
|
13
14
|
APP_ROLE_RE, checkSecurity, currentDef, MASK_LABELS, MASKS, maskedColumns, readSecurity, securitySql, sees,
|
|
14
15
|
type ColumnMask, type SecurityDef, type TableSecurity,
|
|
@@ -55,64 +56,65 @@ function fromForm(b: Body, cur: TableSecurity): SecurityDef {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function form(s: Session, cur: TableSecurity, def: SecurityDef, opts: { ref: string; sql: string[]; warnings: string[]; errors: string[]; output: Raw | '' }): Raw {
|
|
59
|
+
const tx = tr(s);
|
|
58
60
|
const chk = (x: boolean) => (x ? raw(' checked') : '');
|
|
59
61
|
const all = cur.columns.map((c) => c.name);
|
|
60
62
|
const v = def.view;
|
|
61
63
|
const masked = new Set(maskedColumns(v));
|
|
62
64
|
const objUrl = `${BASE}/sql/objects?${new URLSearchParams({ schema: cur.schema, k: 'table', o: opts.ref })}`;
|
|
63
65
|
const policyUrl = `${BASE}/sql/objects/action?${new URLSearchParams({ schema: cur.schema, k: 'table', o: opts.ref, do: 'policy' })}`;
|
|
64
|
-
const rows = html`<div class="table-wrap"><table class="report ds-table"><caption class="sr-only"
|
|
65
|
-
<thead><tr><th scope="col"
|
|
66
|
+
const rows = html`<div class="table-wrap"><table class="report ds-table"><caption class="sr-only">${tx('ds.roles_and_columns')}</caption>
|
|
67
|
+
<thead><tr><th scope="col">${tx('ds.db_role')}</th>${all.map((c) => html`<th scope="col"><code>${c}</code>${masked.has(c) ? html` <span class="badge" title="${tx('ds.masked_in', { view: v.name })}">${tx('ds.masked')}</span>` : ''}</th>`)}</tr></thead>
|
|
66
68
|
<tbody>${cur.grants.length ? def.grants.map((g, i) => {
|
|
67
69
|
const info = cur.grants[i];
|
|
68
|
-
return html`<tr><th scope="row"><code>${g.role === 'PUBLIC' ? 'PUBLIC' : g.role}</code>${info.apps.length ? html`<br><small class="muted"
|
|
69
|
-
${all.map((c, j) => html`<td class="ds-cell"><label class="check"><input type="checkbox" name="g.${i}.s.${j}"${chk(g.select.includes(c))}> read</label>
|
|
70
|
-
<label class="check"><input type="checkbox" name="g.${i}.u.${j}"${chk(g.update.includes(c))}> update</label></td>`)}</tr>`;
|
|
71
|
-
}) : html`<tr><td colspan="${all.length + 1}" class="muted"
|
|
72
|
-
${cur.excluded.length ? html`<p class="muted small"
|
|
73
|
-
const maskRows = html`<div class="table-wrap"><table class="report ds-table"><caption class="sr-only"
|
|
74
|
-
<thead><tr><th scope="col"
|
|
70
|
+
return html`<tr><th scope="row"><code>${g.role === 'PUBLIC' ? 'PUBLIC' : g.role}</code>${info.apps.length ? html`<br><small class="muted">${tx('ds.runs', { apps: info.apps.join(', ') })}</small>` : ''}</th>
|
|
71
|
+
${all.map((c, j) => html`<td class="ds-cell"><label class="check"><input type="checkbox" name="g.${i}.s.${j}"${chk(g.select.includes(c))}> ${tx('ds.read')}</label>
|
|
72
|
+
<label class="check"><input type="checkbox" name="g.${i}.u.${j}"${chk(g.update.includes(c))}> ${tx('ds.update')}</label></td>`)}</tr>`;
|
|
73
|
+
}) : html`<tr><td colspan="${all.length + 1}" class="muted">${tx('ds.no_grants')}</td></tr>`}</tbody></table></div>
|
|
74
|
+
${cur.excluded.length ? html`<p class="muted small">${tx('ds.excluded')} ${cur.excluded.map((x) => html`<code>${x.role}</code> (${x.why}) `)}</p>` : ''}`;
|
|
75
|
+
const maskRows = html`<div class="table-wrap"><table class="report ds-table"><caption class="sr-only">${tx('ds.masks_per_column')}</caption>
|
|
76
|
+
<thead><tr><th scope="col">${tx('ds.column')}</th><th scope="col">${tx('ds.mask')}</th>${v.roles.map((r) => html`<th scope="col">${tx('ds.role_sees', { role: r })}</th>`)}<th scope="col">${tx('ds.others_see')}</th></tr></thead>
|
|
75
77
|
<tbody>${all.map((c, j) => {
|
|
76
78
|
const m = v.cols[c];
|
|
77
79
|
return html`<tr><th scope="row"><code>${c}</code> <small class="muted">${cur.columns[j].type}</small></th>
|
|
78
|
-
<td><select name="m.${j}.mask" aria-label="
|
|
79
|
-
${v.roles.map((r, k) => html`<td class="ds-c"><input type="checkbox" aria-label="${r
|
|
80
|
-
<td class="ds-c"><input type="checkbox" aria-label="
|
|
80
|
+
<td><select name="m.${j}.mask" aria-label="${tx('ds.aria_mask', { column: c })}"><option value="">${tx('ds.not_masked')}</option>${MASKS.map((x) => html`<option value="${x}"${m?.mask === x ? raw(' selected') : ''}>${tx.dyn(`ds.mask.${x}`, MASK_LABELS[x])}</option>`)}</select></td>
|
|
81
|
+
${v.roles.map((r, k) => html`<td class="ds-c"><input type="checkbox" aria-label="${tx('ds.aria_role_sees', { role: r, column: c })}" name="m.${j}.see.${k}"${chk(!!m && sees(v, c, r))}></td>`)}
|
|
82
|
+
<td class="ds-c"><input type="checkbox" aria-label="${tx('ds.aria_others_see', { column: c })}" name="m.${j}.others"${chk(!!m && m.others)}></td></tr>`;
|
|
81
83
|
})}</tbody></table></div>`;
|
|
82
84
|
return html`<form method="post" action="${BASE}/sql/objects/security" class="ds">${csrf(s)}
|
|
83
85
|
<input type="hidden" name="ref" value="${opts.ref}"><input type="hidden" name="shown_roles" value="${v.roles.join(', ')}">
|
|
84
86
|
${opts.errors.length ? html`<div class="alert alert-error" role="alert"><ul>${opts.errors.map((e) => html`<li>${e}</li>`)}</ul></div>` : ''}
|
|
85
|
-
<section class="ds-section"><h2>${icon('shield')}
|
|
86
|
-
<p
|
|
87
|
-
|
|
88
|
-
${cur.policies.length ? html`<div class="table-wrap"><table class="report"><thead><tr><th
|
|
89
|
-
${cur.policies.map((p) => html`<tr><td>${p.name}${p.permissive === 'RESTRICTIVE' ? html` <span class="badge"
|
|
90
|
-
: html`<p class="muted"
|
|
87
|
+
<section class="ds-section"><h2>${icon('shield')} ${tx('ds.rows')}</h2>
|
|
88
|
+
<p>${tx.rich('ds.rls_is', { state: raw(`<strong>${esc(cur.rls ? (cur.forceRls ? tx('ds.rls_on_owner') : tx('ds.rls_on')) : tx('ds.rls_off'))}</strong>`) })}${cur.rls ? '' : `: ${tx('ds.rls_off_detail')}`}.
|
|
89
|
+
${tx.rich('ds.rls_how', { props: raw(`<a href="${BASE}/sql/objects/table?${new URLSearchParams({ o: opts.ref })}">${esc(tx('ds.table_properties'))}</a>`), policy: raw(`<a href="${policyUrl}">${esc(tx('ds.add_policy'))}</a>`) })}</p>
|
|
90
|
+
${cur.policies.length ? html`<div class="table-wrap"><table class="report"><thead><tr><th>${tx('ds.policy')}</th><th>${tx('ds.command')}</th><th>${tx('ds.roles')}</th><th>Using</th><th>With check</th></tr></thead><tbody>
|
|
91
|
+
${cur.policies.map((p) => html`<tr><td>${p.name}${p.permissive === 'RESTRICTIVE' ? html` <span class="badge">${tx('ds.restrictive')}</span>` : ''}</td><td>${p.command}</td><td><code>${p.roles}</code></td><td><code>${p.using ?? ''}</code></td><td><code>${p.check ?? ''}</code></td></tr>`)}</tbody></table></div>`
|
|
92
|
+
: html`<p class="muted">${cur.rls ? tx('ds.no_policies_rls') : tx('ds.no_policies')}</p>`}
|
|
91
93
|
</section>
|
|
92
|
-
<section class="ds-section"><h2>${icon('table')}
|
|
93
|
-
<p class="muted"
|
|
94
|
+
<section class="ds-section"><h2>${icon('table')} ${tx('ds.columns')}</h2>
|
|
95
|
+
<p class="muted">${tx('ds.columns_help')}</p>
|
|
94
96
|
${rows}
|
|
95
97
|
</section>
|
|
96
|
-
<section class="ds-section"><h2>${icon('eye')}
|
|
97
|
-
<p class="muted"
|
|
98
|
-
<label class="check"><input type="checkbox" name="view"${chk(v.enabled)}>
|
|
98
|
+
<section class="ds-section"><h2>${icon('eye')} ${tx('ds.values')}</h2>
|
|
99
|
+
<p class="muted">${tx.code('ds.values_help')}</p>
|
|
100
|
+
<label class="check"><input type="checkbox" name="view"${chk(v.enabled)}> ${tx('ds.masking_view')}</label>
|
|
99
101
|
<div class="form-grid">
|
|
100
|
-
<div class="field"><label class="label" for="ds-view"
|
|
101
|
-
<div class="field"><label class="label" for="ds-roles"
|
|
102
|
-
<small class="help"
|
|
102
|
+
<div class="field"><label class="label" for="ds-view">${tx('ds.view_name')}</label><input id="ds-view" name="view_name" value="${v.name}" autocomplete="off" spellcheck="false"></div>
|
|
103
|
+
<div class="field"><label class="label" for="ds-roles">${tx('ds.app_roles')}</label><input id="ds-roles" name="roles" value="${v.roles.join(', ')}" autocomplete="off" spellcheck="false">
|
|
104
|
+
<small class="help">${tx('ds.app_roles_help')}</small></div>
|
|
103
105
|
</div>
|
|
104
106
|
${maskRows}
|
|
105
107
|
</section>
|
|
106
108
|
<section class="ds-section"><h2>${icon('code')} SQL</h2>
|
|
107
109
|
${opts.warnings.length ? html`<div class="alert alert-warning" role="status"><ul>${opts.warnings.map((w) => html`<li>${w}</li>`)}</ul></div>` : ''}
|
|
108
|
-
${opts.sql.length ? html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="SQL">${opts.sql.join(';\n\n')};</pre>` : html`<p class="muted"
|
|
110
|
+
${opts.sql.length ? html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="SQL">${opts.sql.join(';\n\n')};</pre>` : html`<p class="muted">${tx('ds.no_changes')}</p>`}
|
|
109
111
|
${opts.output}
|
|
110
112
|
</section>
|
|
111
113
|
<div class="buttons">
|
|
112
|
-
<button class="btn" name="mode" value="preview">${icon('eye')}
|
|
113
|
-
<button class="btn btn-hot" name="mode" value="apply">${icon('check')}
|
|
114
|
-
<button class="btn" name="mode" value="worksheet">${icon('code')}
|
|
115
|
-
<a class="btn" href="${objUrl}"
|
|
114
|
+
<button class="btn" name="mode" value="preview">${icon('eye')} ${tx('ds.preview_sql')}</button>
|
|
115
|
+
<button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
|
|
116
|
+
<button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
|
|
117
|
+
<a class="btn" href="${objUrl}">${tx('common.cancel')}</a>
|
|
116
118
|
</div></form>`;
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -139,7 +141,7 @@ async function page(req: Req, reply: FastifyReply) {
|
|
|
139
141
|
if (!errors.length) ({ statements: sql, warnings } = securitySql(def, cur));
|
|
140
142
|
if (!errors.length && b.mode === 'worksheet') {
|
|
141
143
|
s.state.__SQLDRAFT = sql.length ? `${sql.join(';\n\n')};` : '-- no changes';
|
|
142
|
-
flash(s, '
|
|
144
|
+
flash(s, tr(s)('te.review'));
|
|
143
145
|
return back(reply, s, `${BASE}/sql/worksheet`);
|
|
144
146
|
}
|
|
145
147
|
if (!errors.length && b.mode === 'apply' && sql.length) {
|
|
@@ -163,16 +165,16 @@ async function page(req: Req, reply: FastifyReply) {
|
|
|
163
165
|
clearCompletions();
|
|
164
166
|
if (!failed) {
|
|
165
167
|
await recordChanges({ workspaceId: currentWorkspace(s), author: s.username ?? '?', source: 'table', name: `security_${cur.table}`.slice(0, 63), defaultSchema: cur.schema, statements: [joinStatements(sql)] });
|
|
166
|
-
flash(s,
|
|
168
|
+
flash(s, sql.length === 1 ? tr(s)('ds.changed_1', { table: `${cur.schema}.${cur.table}` }) : tr(s)('ds.changed', { table: `${cur.schema}.${cur.table}`, n: sql.length }));
|
|
167
169
|
return back(reply, s, `${BASE}/sql/objects/security?${new URLSearchParams({ o: ref })}`);
|
|
168
170
|
}
|
|
169
|
-
output = html`<div class="alert alert-error" role="alert"><p
|
|
171
|
+
output = html`<div class="alert alert-error" role="alert"><p>${tr(s)('ds.rolled_back')}</p><p>${failed}</p><pre class="source" data-code-view="sql" tabindex="0" aria-label="${tr(s)('ds.failed_statement')}">${at}</pre></div>`;
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
|
-
const title =
|
|
174
|
+
const title = tr(s)('ds.title', { table: `${cur.schema}.${cur.table}` });
|
|
173
175
|
const objUrl = `${BASE}/sql/objects?${new URLSearchParams({ schema: cur.schema, k: 'table', o: ref })}`;
|
|
174
176
|
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}${form(s, cur, def, { ref, sql, warnings, errors, output })}`;
|
|
175
|
-
return send(reply, s, shell(s, title, [['
|
|
177
|
+
return send(reply, s, shell(s, title, [[tr(s)('rail.sql_workshop'), `${BASE}/sql`], [tr(s)('sql.tool.objects'), `${BASE}/sql/objects`], [`${cur.schema}.${cur.table}`, objUrl], [tr(s)('ds.security')]], main, 'sql'));
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
export async function dataSecurityRoutes(app: FastifyInstance) {
|