pgkiln 0.82.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/153_component_events.sql +21 -0
- package/docs/apex-feature-parity.md +3 -3
- package/docs/guide/03-builder.md +8 -1
- package/docs/guide/04-pages-and-regions.md +2 -1
- package/docs/guide/07-dynamic-actions.md +27 -0
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +9 -1
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/app.js +85 -4
- package/public/builder.css +25 -2
- package/public/data-modeler.js +8 -5
- package/public/sql-monitor.js +5 -2
- 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/components.ts +5 -4
- 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/document-view.ts +11 -11
- package/src/builder/i18n/de.ts +1627 -0
- package/src/builder/i18n/en.ts +1627 -0
- package/src/builder/i18n/es.ts +1627 -0
- package/src/builder/i18n/fr.ts +1627 -0
- package/src/builder/i18n/it.ts +1627 -0
- package/src/builder/i18n/ja.ts +1627 -0
- package/src/builder/i18n/ko.ts +1627 -0
- package/src/builder/i18n/nl.ts +1627 -0
- package/src/builder/i18n/pt.ts +1627 -0
- package/src/builder/i18n/zh-cn.ts +1627 -0
- package/src/builder/i18n/zh-tw.ts +1627 -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-settings.ts +201 -199
- 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/metadata.ts +3 -1
- package/src/runtime/calendar.ts +3 -2
- package/src/runtime/context.ts +2 -0
- package/src/runtime/grid.ts +1 -0
- package/src/runtime/maps.ts +7 -4
- package/src/runtime/render.ts +6 -1
- package/src/runtime/routes.ts +4 -0
- package/src/runtime/tree.ts +4 -3
package/src/builder/dataload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
2
2
|
import { owner } from '../db.ts';
|
|
3
|
-
import { html, raw, type Raw } from '../html.ts';
|
|
3
|
+
import { esc, html, raw, type Raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import {
|
|
6
6
|
applyMapping,
|
|
@@ -27,6 +27,7 @@ import type { Session } from '../session.ts';
|
|
|
27
27
|
import type { ComponentSpec } from './components.ts';
|
|
28
28
|
import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
|
|
29
29
|
import { appWorkspace, currentWorkspace, workshopDb } from './workspaces.ts';
|
|
30
|
+
import { tr, translatorFor, type BuilderTranslate } from './i18n.ts';
|
|
30
31
|
|
|
31
32
|
// SQL Workshop → Load Data: upload a CSV/TSV/XLSX/JSON/XML file, preview it,
|
|
32
33
|
// and load it into a new table (column types inferred) or an existing one
|
|
@@ -125,21 +126,21 @@ export function preview(sheet: Sheet) {
|
|
|
125
126
|
<tbody>${sheet.rows.slice(0, PREVIEW_ROWS).map((r) => html`<tr>${r.map((v) => html`<td>${v === null ? html`<span class="null">null</span>` : v}</td>`)}</tr>`)}</tbody></table></div>`;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
export function resultHtml(r: LoadResult, failed: boolean) {
|
|
129
|
+
export function resultHtml(r: LoadResult, failed: boolean, tx: BuilderTranslate = translatorFor('en')) {
|
|
129
130
|
const errors = r.errors.length
|
|
130
|
-
? html`<div class="table-wrap"><table class="report"><thead><tr><th class="num"
|
|
131
|
+
? html`<div class="table-wrap"><table class="report"><thead><tr><th class="num">${tx('dl.row')}</th><th>${tx('dl.error')}</th></tr></thead>
|
|
131
132
|
<tbody>${r.errors.map((e) => html`<tr><td class="num">${e.row}</td><td>${e.message}</td></tr>`)}</tbody></table></div>
|
|
132
|
-
${r.failed > r.errors.length ? html`<p class="muted"
|
|
133
|
+
${r.failed > r.errors.length ? html`<p class="muted">${tx('dl.first_errors', { n: r.errors.length })}</p>` : ''}`
|
|
133
134
|
: '';
|
|
134
135
|
const summary = failed
|
|
135
|
-
? html`<div class="alert alert-error" role="alert">${r.failed}${r.errors.length >= 100 ? '+' : ''}
|
|
136
|
-
: html`<div class="alert alert-success" role="status">${r.table}: ${r.inserted}
|
|
136
|
+
? html`<div class="alert alert-error" role="alert">${tx('dl.failed', { n: `${r.failed}${r.errors.length >= 100 ? '+' : ''}` })}</div>`
|
|
137
|
+
: html`<div class="alert alert-success" role="status">${r.table}: ${tx('dl.inserted', { n: r.inserted })}${r.updated ? `, ${tx('dl.updated', { n: r.updated })}` : ''}${r.failed ? `, ${tx('dl.skipped', { n: r.failed })}` : ''}.</div>`;
|
|
137
138
|
return html`${summary}${errors}`;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
export async function dataLoadRoutes(app: FastifyInstance) {
|
|
141
142
|
const page = (s: Session, reply: FastifyReply, main: Raw) =>
|
|
142
|
-
send(reply, s, shell(s, '
|
|
143
|
+
send(reply, s, shell(s, tr(s)('sql.tool.load'), [[tr(s)('rail.sql_workshop'), `${BASE}/sql`], [tr(s)('sql.tool.load')]], html`<h1 class="u-mb1">${tr(s)('rail.sql_workshop')}</h1>${workshopTabs('load', s)}${main}`, 'sql'));
|
|
143
144
|
|
|
144
145
|
// step 1: choose a file
|
|
145
146
|
app.get(`${BASE}/sql/load`, async (req: Req, reply) => {
|
|
@@ -150,28 +151,28 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
150
151
|
|
|
151
152
|
const uploadForm = async (s: Session, error?: string, chosen?: string) => {
|
|
152
153
|
const defs = await definitions(s);
|
|
154
|
+
const tx = tr(s);
|
|
153
155
|
return region(
|
|
154
|
-
'
|
|
156
|
+
tx('dl.from_file'),
|
|
155
157
|
html`${error ? html`<div class="alert alert-error" role="alert">${error}</div>` : ''}
|
|
156
|
-
<p class="muted"
|
|
157
|
-
|
|
158
|
-
To download a table or a query as a file, use <a href="${BASE}/sql/unload">Unload Data</a>.</p>
|
|
158
|
+
<p class="muted">${tx('dl.intro', { mb: MAX_MB })}
|
|
159
|
+
${tx.rich('dl.intro_unload', { link: raw(`<a href="${BASE}/sql/unload">${esc(tx('sql.tool.unload'))}</a>`) })}</p>
|
|
159
160
|
<form method="post" enctype="multipart/form-data">${csrf(s)}
|
|
160
161
|
<div class="form-grid">
|
|
161
|
-
<div class="field"><label class="label" for="f_file"
|
|
162
|
+
<div class="field"><label class="label" for="f_file">${tx('dl.file')}</label>
|
|
162
163
|
<input type="file" id="f_file" name="file" accept=".csv,.tsv,.txt,.xlsx,.json,.jsonl,.xml,text/csv,application/json,application/xml,text/xml" required></div>
|
|
163
164
|
<div class="field"><span class="label" aria-hidden="true"></span>
|
|
164
|
-
<label class="check"><input type="checkbox" name="headers" value="true" checked>
|
|
165
|
-
<div class="field"><label class="label" for="f_row_tag"
|
|
166
|
-
<input id="f_row_tag" name="row_tag" placeholder="
|
|
167
|
-
<small class="help"
|
|
168
|
-
<div class="field"><label class="label" for="f_definition"
|
|
169
|
-
<select id="f_definition" name="definition"><option value=""
|
|
165
|
+
<label class="check"><input type="checkbox" name="headers" value="true" checked> ${tx('dl.headers')}</label></div>
|
|
166
|
+
<div class="field"><label class="label" for="f_row_tag">${tx('dl.row_tag')}</label>
|
|
167
|
+
<input id="f_row_tag" name="row_tag" placeholder="${tx('dl.row_tag_ph')}">
|
|
168
|
+
<small class="help">${tx('dl.row_tag_help')}</small></div>
|
|
169
|
+
<div class="field"><label class="label" for="f_definition">${tx('comp.data_load_def')}</label>
|
|
170
|
+
<select id="f_definition" name="definition"><option value="">${tx('dl.no_definition')}</option>${defs.map(
|
|
170
171
|
(d) => html`<option value="${d.id}"${String(d.id) === chosen ? raw(' selected') : ''}>${d.app_name} · ${d.name} → ${d.table_name}</option>`,
|
|
171
172
|
)}</select>
|
|
172
|
-
<small class="help"
|
|
173
|
+
<small class="help">${tx('dl.definition_help')}</small></div>
|
|
173
174
|
</div>
|
|
174
|
-
<div class="buttons"><button class="btn btn-hot">${icon('upload')}
|
|
175
|
+
<div class="buttons"><button class="btn btn-hot">${icon('upload')} ${tx('dl.next')}</button></div>
|
|
175
176
|
</form>`,
|
|
176
177
|
);
|
|
177
178
|
};
|
|
@@ -185,8 +186,8 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
185
186
|
}
|
|
186
187
|
const s = await developer(req, reply);
|
|
187
188
|
if (!s) return;
|
|
188
|
-
if (!file) return page(s, reply, await uploadForm(s, '
|
|
189
|
-
if (file.truncated) return page(s, reply, await uploadForm(s,
|
|
189
|
+
if (!file) return page(s, reply, await uploadForm(s, tr(s)('dl.e.no_file')));
|
|
190
|
+
if (file.truncated) return page(s, reply, await uploadForm(s, tr(s)('dl.e.too_large', { mb: MAX_MB })));
|
|
190
191
|
const def = await definition(s, req.body?.definition);
|
|
191
192
|
const rowTag = (req.body?.row_tag ?? '').trim();
|
|
192
193
|
try {
|
|
@@ -220,11 +221,14 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
220
221
|
if (e instanceof LoadError) return page(s, reply, await uploadForm(s, e.message));
|
|
221
222
|
throw e;
|
|
222
223
|
}
|
|
224
|
+
const tx = tr(s);
|
|
223
225
|
const action = `${BASE}/sql/load/${req.params.id}`;
|
|
224
226
|
const hidden = html`${csrf(s)}<input type="hidden" name="h" value="${headers ? '1' : '0'}"><input type="hidden" name="rt" value="${rowTag}">`;
|
|
225
|
-
const info = `${f.filename}: ${sheet.rows.length
|
|
226
|
-
sheet.format === 'xlsx' ? ', Excel' : sheet.format === 'json' ? ', JSON' : sheet.format === 'xml' ? `,
|
|
227
|
+
const info = `${f.filename}: ${tx('dl.info', { rows: sheet.rows.length, columns: sheet.headers.length })}${
|
|
228
|
+
sheet.format === 'xlsx' ? ', Excel' : sheet.format === 'json' ? ', JSON' : sheet.format === 'xml' ? `, ${tx('dl.xml_rows', { path: `<${sheet.rowPath}>` })}` : `, ${tx('dl.delimiter', { d: sheet.delimiter === '\t' ? tx('dl.tab') : `“${sheet.delimiter}”` })}`
|
|
227
229
|
}`;
|
|
230
|
+
const firstShown = sheet.rows.length > PREVIEW_ROWS ? ` ${tx('dl.first_shown', { n: PREVIEW_ROWS })}` : '';
|
|
231
|
+
const anotherFile = html`<a href="${BASE}/sql/load">${tx('dl.another_file')}</a>`;
|
|
228
232
|
if (def) {
|
|
229
233
|
let mapped: Sheet = sheet;
|
|
230
234
|
let problem = '';
|
|
@@ -234,34 +238,34 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
234
238
|
if (!(e instanceof LoadError)) throw e;
|
|
235
239
|
problem = e.message;
|
|
236
240
|
}
|
|
237
|
-
const defHtml = html`<p
|
|
238
|
-
${def.columns?.length ? '' : '
|
|
239
|
-
${problem ? html`<div class="alert alert-error" role="alert">${problem}</div>` : html`<p class="muted"
|
|
241
|
+
const defHtml = html`<p>${tx.rich('dl.def_into', { def: raw(`<a href="${BASE}/apps/${def.app_id}/shared?c=data_load_def-${def.id}">${esc(def.name)}</a>`), table: raw(`<b>${esc(def.table_name)}</b>`), mode: def.mode })}${def.skip_errors ? `, ${tx('dl.def_skipping')}` : ''}.
|
|
242
|
+
${def.columns?.length ? '' : ` ${tx('dl.def_by_name')}`}</p>
|
|
243
|
+
${problem ? html`<div class="alert alert-error" role="alert">${problem}</div>` : html`<p class="muted">${mapped.rows.length > PREVIEW_ROWS ? tx('dl.after_mapping_first', { n: PREVIEW_ROWS }) : tx('dl.after_mapping')}</p>${preview(mapped)}`}
|
|
240
244
|
<form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="definition"><input type="hidden" name="d" value="${def.id}">
|
|
241
|
-
<div class="buttons"><button class="btn btn-hot"${problem ? raw(' disabled') : ''}>${icon('upload')}
|
|
245
|
+
<div class="buttons"><button class="btn btn-hot"${problem ? raw(' disabled') : ''}>${icon('upload')} ${tx('dl.load_into', { table: def.table_name })}</button></div></form>`;
|
|
242
246
|
return page(
|
|
243
247
|
s,
|
|
244
248
|
reply,
|
|
245
249
|
html`${result}
|
|
246
|
-
${region('
|
|
250
|
+
${region(tx('dl.preview'), html`<p class="muted">${info}${firstShown}. ${anotherFile}</p>${preview(sheet)}`)}
|
|
247
251
|
<div class="u-spacer"></div>
|
|
248
|
-
${region(
|
|
252
|
+
${region(tx('dl.load_with', { name: def.name }), defHtml)}`,
|
|
249
253
|
);
|
|
250
254
|
}
|
|
251
|
-
const skip = html`<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="skip_errors" value="true">
|
|
255
|
+
const skip = html`<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="skip_errors" value="true"> ${tx('dl.skip_errors')}</label></div>`;
|
|
252
256
|
|
|
253
257
|
const newCols = suggestColumns(sheet);
|
|
254
258
|
const newTable = html`<form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="new">
|
|
255
|
-
<div class="form-grid"><div class="field"><label class="label" for="f_new_table"
|
|
259
|
+
<div class="form-grid"><div class="field"><label class="label" for="f_new_table">${tx('dl.table_name')}</label>
|
|
256
260
|
<input id="f_new_table" name="new_table" value="${tableName(f.filename)}" required>
|
|
257
|
-
<small class="help"
|
|
258
|
-
<div class="table-wrap"><table class="report"><thead><tr><th
|
|
261
|
+
<small class="help">${tx.code('dl.table_name_help')}</small></div></div>
|
|
262
|
+
<div class="table-wrap"><table class="report"><thead><tr><th>${tx('dl.file_column')}</th><th>${tx('dl.column_name')}</th><th>${tx('dl.type')}</th></tr></thead><tbody>
|
|
259
263
|
${newCols.map((c) => html`<tr><td>${sheet.headers[c.index]}</td>
|
|
260
|
-
<td><input name="name_${c.index}" value="${c.name}" aria-label="
|
|
261
|
-
<td><select name="type_${c.index}" aria-label="
|
|
264
|
+
<td><input name="name_${c.index}" value="${c.name}" aria-label="${tx('dl.aria_name', { column: sheet.headers[c.index] })}"></td>
|
|
265
|
+
<td><select name="type_${c.index}" aria-label="${tx('dl.aria_type', { column: sheet.headers[c.index] })}">${COLUMN_TYPES.map((t) => html`<option${t === c.type ? raw(' selected') : ''}>${t}</option>`)}</select></td></tr>`)}
|
|
262
266
|
</tbody></table></div>
|
|
263
267
|
${skip}
|
|
264
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
268
|
+
<div class="buttons"><button class="btn btn-hot">${tx('dl.create_and_load')}</button></div></form>`;
|
|
265
269
|
|
|
266
270
|
const db = await workshopDb(s);
|
|
267
271
|
const tables = (
|
|
@@ -279,44 +283,44 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
279
283
|
const auto = autoMap(sheet.headers, cols);
|
|
280
284
|
const hasPk = cols.some((c) => c.pk);
|
|
281
285
|
mapping = html`<form method="post" action="${action}">${hidden}<input type="hidden" name="target" value="existing"><input type="hidden" name="table" value="${chosen}">
|
|
282
|
-
<div class="table-wrap"><table class="report"><thead><tr><th
|
|
286
|
+
<div class="table-wrap"><table class="report"><thead><tr><th>${tx('dl.file_column')}</th><th>${tx('dl.table_column')}</th></tr></thead><tbody>
|
|
283
287
|
${sheet.headers.map((h, i) => {
|
|
284
288
|
const cur = auto.find((m) => m.index === i)?.column ?? '';
|
|
285
|
-
return html`<tr><td>${h}</td><td><select name="map_${i}" aria-label="
|
|
289
|
+
return html`<tr><td>${h}</td><td><select name="map_${i}" aria-label="${tx('dl.aria_map', { column: h })}"><option value="">${tx('dl.skip')}</option>${cols
|
|
286
290
|
.filter((c) => !c.generated)
|
|
287
|
-
.map((c) => html`<option value="${c.name}"${c.name === cur ? raw(' selected') : ''}>${c.name} (${c.type}${c.pk ? '
|
|
291
|
+
.map((c) => html`<option value="${c.name}"${c.name === cur ? raw(' selected') : ''}>${c.name} (${c.type}${c.pk ? `, ${tx('dl.key')}` : ''})</option>`)}</select></td></tr>`;
|
|
288
292
|
})}
|
|
289
293
|
</tbody></table></div>
|
|
290
|
-
<fieldset class="field"><legend class="label"
|
|
291
|
-
<label class="check"><input type="radio" name="mode" value="append" checked>
|
|
292
|
-
<label class="check"><input type="radio" name="mode" value="merge"${hasPk ? '' : raw(' disabled')}>
|
|
293
|
-
<label class="check"><input type="radio" name="mode" value="replace">
|
|
294
|
+
<fieldset class="field"><legend class="label">${tx('dl.mode')}</legend><div class="radio-group">
|
|
295
|
+
<label class="check"><input type="radio" name="mode" value="append" checked> ${tx('dl.mode_append')}</label>
|
|
296
|
+
<label class="check"><input type="radio" name="mode" value="merge"${hasPk ? '' : raw(' disabled')}> ${tx('dl.mode_merge')}</label>
|
|
297
|
+
<label class="check"><input type="radio" name="mode" value="replace"> ${tx('dl.mode_replace')}</label>
|
|
294
298
|
</div></fieldset>
|
|
295
299
|
${skip}
|
|
296
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
297
|
-
<details class="u-mt1"><summary
|
|
298
|
-
<p class="muted"
|
|
300
|
+
<div class="buttons"><button class="btn btn-hot">${tx('dl.load_into', { table: chosen })}</button></div>
|
|
301
|
+
<details class="u-mt1"><summary>${tx('dl.save_mapping')}</summary>
|
|
302
|
+
<p class="muted">${tx('dl.save_mapping_help')}</p>
|
|
299
303
|
<div class="form-grid">
|
|
300
|
-
<div class="field"><label class="label" for="f_def_app"
|
|
304
|
+
<div class="field"><label class="label" for="f_def_app">${tx('rm.application')}</label>
|
|
301
305
|
<select id="f_def_app" name="def_app">${apps.map((a) => html`<option value="${a.id}">${a.name}</option>`)}</select></div>
|
|
302
|
-
<div class="field"><label class="label" for="f_def_name"
|
|
306
|
+
<div class="field"><label class="label" for="f_def_name">${tx('dl.name')}</label><input id="f_def_name" name="def_name" placeholder="${tx('rm.eg', { example: defName(f.filename) })}"></div>
|
|
303
307
|
</div>
|
|
304
|
-
<div class="buttons"><button class="btn" name="target" value="save_definition">${icon('check')}
|
|
308
|
+
<div class="buttons"><button class="btn" name="target" value="save_definition">${icon('check')} ${tx('dl.save_definition')}</button></div>
|
|
305
309
|
</details></form>`;
|
|
306
310
|
}
|
|
307
311
|
const existing = html`<form method="get" action="${action}" class="search u-mwnone">
|
|
308
312
|
<input type="hidden" name="h" value="${headers ? '1' : '0'}"><input type="hidden" name="rt" value="${rowTag}">
|
|
309
|
-
<select name="table" aria-label="
|
|
310
|
-
<button class="btn"
|
|
313
|
+
<select name="table" aria-label="${tx('dl.table')}"><option value="">${tx('dl.choose_table')}</option>${tables.map((t) => html`<option${t === chosen ? raw(' selected') : ''}>${t}</option>`)}</select>
|
|
314
|
+
<button class="btn">${tx('dl.map_columns')}</button></form>${mapping}`;
|
|
311
315
|
|
|
312
316
|
return page(
|
|
313
317
|
s,
|
|
314
318
|
reply,
|
|
315
319
|
html`${result}
|
|
316
|
-
${region(
|
|
320
|
+
${region(tx('dl.preview'), html`<p class="muted">${info}${firstShown}. ${anotherFile}</p>${preview(sheet)}`)}
|
|
317
321
|
<div class="columns u-mt1">
|
|
318
|
-
${region('
|
|
319
|
-
${region('
|
|
322
|
+
${region(tx('dl.into_new'), newTable)}
|
|
323
|
+
${region(tx('dl.into_existing'), existing)}
|
|
320
324
|
</div>`,
|
|
321
325
|
);
|
|
322
326
|
};
|
|
@@ -344,20 +348,20 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
344
348
|
const mode = ['append', 'replace', 'merge'].includes(b.mode ?? '') ? b.mode : 'append';
|
|
345
349
|
const format = sheet instanceof Error ? 'auto' : sheet.format;
|
|
346
350
|
try {
|
|
347
|
-
if (!/^\d{1,9}$/.test(String(b.def_app ?? '')) || (await appWorkspace(String(b.def_app))) !== currentWorkspace(s)) throw new LoadError('
|
|
351
|
+
if (!/^\d{1,9}$/.test(String(b.def_app ?? '')) || (await appWorkspace(String(b.def_app))) !== currentWorkspace(s)) throw new LoadError(tr(s)('rm.e.choose_app'));
|
|
348
352
|
const r = await owner.one(
|
|
349
353
|
`insert into meta.data_load_def (app_id, name, table_name, format, headers, row_tag, mode, skip_errors, columns)
|
|
350
354
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id, app_id`,
|
|
351
355
|
[b.def_app, name, b.table, format, headers, format === 'xml' && !(sheet instanceof Error) ? sheet.rowPath : null, mode, b.skip_errors === 'true', JSON.stringify(columns)],
|
|
352
356
|
);
|
|
353
|
-
flash(s,
|
|
357
|
+
flash(s, tr(s)('dl.def_saved', { name }));
|
|
354
358
|
return back(reply, s, `${BASE}/apps/${r!.app_id}/shared?c=data_load_def-${r!.id}`);
|
|
355
359
|
} catch (e) {
|
|
356
360
|
const code = (e as { code?: string }).code;
|
|
357
361
|
if (!(e instanceof LoadError) && !code?.startsWith('23') && code !== '22P02') throw e;
|
|
358
362
|
reply.code(422);
|
|
359
|
-
return targetPage(req, reply, s, html`<div class="alert alert-error" role="alert"
|
|
360
|
-
code === '23505' ?
|
|
363
|
+
return targetPage(req, reply, s, html`<div class="alert alert-error" role="alert">${tr(s)('dl.def_not_saved', {
|
|
364
|
+
reason: code === '23505' ? tr(s)('dl.def_exists', { name }) : (e as Error).message })}</div>`);
|
|
361
365
|
}
|
|
362
366
|
}
|
|
363
367
|
let result: LoadResult | undefined;
|
|
@@ -369,12 +373,12 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
369
373
|
const opts = { skipErrors: b.skip_errors === 'true', firstRow: headers && sheet && (sheet.format === 'csv' || sheet.format === 'xlsx') ? 2 : 1 };
|
|
370
374
|
result = await (await workshopDb(s)).tx(async (c) => {
|
|
371
375
|
if (def) return loadWithDefinition(c, def, f);
|
|
372
|
-
if (!sheet) throw new LoadError('
|
|
376
|
+
if (!sheet) throw new LoadError(tr(s)('dl.e.def_gone'));
|
|
373
377
|
if (b.target === 'new') {
|
|
374
378
|
const cols: NewColumn[] = sheet.headers
|
|
375
379
|
.map((_, index) => ({ index, name: (b[`name_${index}`] ?? '').trim(), type: (b[`type_${index}`] ?? 'text') as NewColumn['type'] }))
|
|
376
380
|
.filter((x) => x.name);
|
|
377
|
-
if (!cols.length) throw new LoadError('
|
|
381
|
+
if (!cols.length) throw new LoadError(tr(s)('dl.e.no_columns'));
|
|
378
382
|
await c.query(createTableSql((b.new_table ?? '').trim(), cols));
|
|
379
383
|
const table = (b.new_table ?? '').trim();
|
|
380
384
|
return loadRows(c, sheet, { ...opts, table, mode: 'append', columns: cols.map((x) => ({ index: x.index, column: x.name })) });
|
|
@@ -395,12 +399,12 @@ export async function dataLoadRoutes(app: FastifyInstance) {
|
|
|
395
399
|
return page(
|
|
396
400
|
s,
|
|
397
401
|
reply,
|
|
398
|
-
html`${resultHtml(result, false)}<div class="buttons">
|
|
399
|
-
<a class="btn btn-hot" href="${BASE}/sql/objects?o=${encodeURIComponent(result.table)}">${icon('table')}
|
|
400
|
-
<a class="btn" href="${BASE}/sql/load">${icon('upload')}
|
|
402
|
+
html`${resultHtml(result, false, tr(s))}<div class="buttons">
|
|
403
|
+
<a class="btn btn-hot" href="${BASE}/sql/objects?o=${encodeURIComponent(result.table)}">${icon('table')} ${tr(s)('dl.view', { table: result.table })}</a>
|
|
404
|
+
<a class="btn" href="${BASE}/sql/load">${icon('upload')} ${tr(s)('dl.load_another')}</a></div>`,
|
|
401
405
|
);
|
|
402
406
|
}
|
|
403
407
|
reply.code(422);
|
|
404
|
-
return targetPage(req, reply, s, result ? resultHtml(result, true) : html`<div class="alert alert-error" role="alert">${error}</div>`);
|
|
408
|
+
return targetPage(req, reply, s, result ? resultHtml(result, true, tr(s)) : html`<div class="alert alert-error" role="alert">${error}</div>`);
|
|
405
409
|
});
|
|
406
410
|
}
|