@adecore/database 0.23.0 → 0.24.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/README.md +2 -2
- package/dist/client/client.js +31 -11
- package/dist/client/types.d.ts +3 -0
- package/dist/console/QueryConsole.js +8 -10
- package/dist/console/SqlEditor.js +5 -10
- package/dist/console/StatementResultView.d.ts +3 -1
- package/dist/console/StatementResultView.js +5 -2
- package/dist/console/shortcuts.d.ts +3 -0
- package/dist/console/shortcuts.js +14 -3
- package/dist/grid/DataGrid.d.ts +3 -1
- package/dist/grid/DataGrid.js +35 -22
- package/dist/grid/GridRowView.js +2 -2
- package/dist/grid/RecordView.js +2 -2
- package/dist/grid/display.d.ts +2 -0
- package/dist/grid/display.js +11 -0
- package/dist/grid/whole-cells.d.ts +9 -0
- package/dist/grid/whole-cells.js +49 -0
- package/dist/host/helper-path.d.ts +2 -1
- package/dist/host/helper-path.js +26 -5
- package/dist/host/host.js +5 -0
- package/dist/host/validate.js +8 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/locales/en.d.ts +601 -0
- package/dist/locales/en.js +600 -0
- package/dist/locales/nl.d.ts +601 -0
- package/dist/locales/nl.js +600 -0
- package/dist/locales.js +3 -2
- package/dist/protocol/messages.d.ts +4 -1
- package/dist/protocol/messages.js +1 -1
- package/dist/table/TableView.js +1 -1
- package/dist/testing/fake.js +24 -14
- package/package.json +8 -8
- package/src/client/client.ts +32 -11
- package/src/client/types.ts +10 -2
- package/src/console/QueryConsole.tsx +10 -9
- package/src/console/SqlEditor.tsx +5 -10
- package/src/console/StatementResultView.tsx +15 -1
- package/src/console/shortcuts.ts +16 -3
- package/src/grid/DataGrid.tsx +47 -24
- package/src/grid/GridRowView.tsx +2 -2
- package/src/grid/RecordView.tsx +2 -2
- package/src/grid/display.ts +13 -0
- package/src/grid/whole-cells.ts +72 -0
- package/src/host/helper-path.ts +42 -4
- package/src/host/host.ts +6 -0
- package/src/host/validate.ts +9 -3
- package/src/index.ts +1 -0
- package/src/locales/en.ts +600 -0
- package/src/locales/nl.ts +600 -0
- package/src/locales.ts +3 -2
- package/src/protocol/messages.ts +21 -4
- package/src/table/TableView.tsx +2 -1
- package/src/testing/fake.ts +29 -14
- package/dist/locales/en.json +0 -594
- package/dist/locales/nl.json +0 -594
- package/src/locales/en.json +0 -594
- package/src/locales/nl.json +0 -594
package/src/grid/display.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatBytes, formatNumeral } from '@adecore/ui/format';
|
|
2
2
|
import type { BinaryValue, Cell, EditValue, ValueKind } from '../protocol/index.ts';
|
|
3
3
|
import { isNumericKind } from '../sql.ts';
|
|
4
|
+
import type { GridColumn } from './types.ts';
|
|
4
5
|
|
|
5
6
|
/* What a cell shows: a cell of a result, or the column's default for a value an edit set to `DEFAULT`. */
|
|
6
7
|
export type Shown = Cell | { readonly kind: 'default' };
|
|
@@ -47,6 +48,18 @@ export const numeralText = (numeral: string | number, notation: NumberNotation):
|
|
|
47
48
|
return formatted === String(numeral).trim() ? String(numeral) : formatted;
|
|
48
49
|
};
|
|
49
50
|
|
|
51
|
+
/* Identifiers are labels, even when the database stores their characters as a number. */
|
|
52
|
+
export function columnNotation(column: GridColumn, notation: NumberNotation): NumberNotation {
|
|
53
|
+
const identifierName = (name: string): boolean => /^(?:id|.+_id)$/i.test(name) || /(?:Id|ID)$/.test(name);
|
|
54
|
+
return column.primaryKey === true ||
|
|
55
|
+
column.foreignKey === true ||
|
|
56
|
+
column.autoIncrement === true ||
|
|
57
|
+
identifierName(column.name) ||
|
|
58
|
+
(column.source !== undefined && identifierName(column.source.column))
|
|
59
|
+
? 'database'
|
|
60
|
+
: notation;
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
const cut = (text: string): string => (text.length > DISPLAY_LIMIT ? `${text.slice(0, DISPLAY_LIMIT)}${ELLIPSIS}` : text);
|
|
51
64
|
|
|
52
65
|
export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation = 'database'): CellView => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Value } from '../protocol/index.ts';
|
|
2
|
+
import { isPreview, shownOfEdit, type Shown } from './display.ts';
|
|
3
|
+
|
|
4
|
+
/* Characters of whole values one copy fetches at most, a byte of a binary value counting as two: past it the copy would hold the clipboard and the session for too long. */
|
|
5
|
+
export const WHOLE_COPY_LIMIT = 16 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/* The helper runs the requests of a session one after another, so more at once would only wait in its queue. */
|
|
8
|
+
const PARALLEL_LOADS = 4;
|
|
9
|
+
|
|
10
|
+
export interface WholeCells {
|
|
11
|
+
readonly rows: readonly (readonly Shown[])[];
|
|
12
|
+
/* The cells that are still previews. */
|
|
13
|
+
readonly previews: number;
|
|
14
|
+
/* Whether the previews stayed because the whole values are larger than `WHOLE_COPY_LIMIT`, rather than because they could not be loaded. */
|
|
15
|
+
readonly tooLarge: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Wanted {
|
|
19
|
+
readonly row: number;
|
|
20
|
+
readonly column: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const wholeSize = (cell: Shown): number => {
|
|
24
|
+
if (cell === null || typeof cell !== 'object' || cell.kind === 'default') {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
return cell.kind === 'longText' ? cell.length : cell.length * 2;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* A block of cells with every preview replaced by the whole value `load` fetches for it, by its row and
|
|
32
|
+
* column in the block. A cell `load` resolves `undefined` for, or rejects for, stays a preview.
|
|
33
|
+
*/
|
|
34
|
+
export const loadWholeCells = async (
|
|
35
|
+
rows: readonly (readonly Shown[])[],
|
|
36
|
+
load: ((row: number, column: number) => Promise<Value | undefined>) | undefined
|
|
37
|
+
): Promise<WholeCells> => {
|
|
38
|
+
const wanted: Wanted[] = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
rows.forEach((cells, row) =>
|
|
41
|
+
cells.forEach((cell, column) => {
|
|
42
|
+
if (isPreview(cell)) {
|
|
43
|
+
wanted.push({ row, column });
|
|
44
|
+
size += wholeSize(cell);
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
if (wanted.length === 0) {
|
|
49
|
+
return { rows, previews: 0, tooLarge: false };
|
|
50
|
+
}
|
|
51
|
+
if (load === undefined || size > WHOLE_COPY_LIMIT) {
|
|
52
|
+
return { rows, previews: wanted.length, tooLarge: load !== undefined };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const filled = rows.map((cells) => [...cells]);
|
|
56
|
+
let previews = 0;
|
|
57
|
+
let next = 0;
|
|
58
|
+
const work = async (): Promise<void> => {
|
|
59
|
+
while (next < wanted.length) {
|
|
60
|
+
const { row, column } = wanted[next]!;
|
|
61
|
+
next += 1;
|
|
62
|
+
const value = await load(row, column).catch(() => undefined);
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
previews += 1;
|
|
65
|
+
} else {
|
|
66
|
+
filled[row]![column] = shownOfEdit(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
await Promise.all(Array.from({ length: Math.min(PARALLEL_LOADS, wanted.length) }, work));
|
|
71
|
+
return { rows: filled, previews, tooLarge: false };
|
|
72
|
+
};
|
package/src/host/helper-path.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { dirname, join, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
export interface HelperPathOptions {
|
|
6
7
|
/* `process.platform` when left out. */
|
|
7
8
|
readonly platform?: string;
|
|
8
9
|
/* `process.arch` when left out. */
|
|
9
10
|
readonly arch?: string;
|
|
11
|
+
/* `process.env` when left out. */
|
|
12
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
10
13
|
/* Resolves a package specifier to a file; the module resolution of this file when left out. */
|
|
11
14
|
readonly resolve?: (specifier: string) => string;
|
|
12
15
|
readonly exists?: (path: string) => boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/*
|
|
16
|
-
* Finds the
|
|
17
|
-
*
|
|
19
|
+
* Finds the helper for this machine, or null when there is none. In order: the path in `ADECORE_DATABASE_HELPER`;
|
|
20
|
+
* the prebuilt binary npm installed in the `@adecore/database-<platform>-<arch>` package, absent on an
|
|
21
|
+
* unsupported platform or an install with optional dependencies off; and, when the package runs from the `src`
|
|
22
|
+
* of a checkout, the release build `helper:build` leaves in that checkout.
|
|
18
23
|
*
|
|
19
24
|
* A binary cannot run from inside an Electron archive. The path is mapped from `app.asar` to
|
|
20
25
|
* `app.asar.unpacked`, so the app must unpack the packages (electron-builder: `asarUnpack` with
|
|
@@ -23,9 +28,29 @@ export interface HelperPathOptions {
|
|
|
23
28
|
export const helperPath = ({
|
|
24
29
|
platform = process.platform,
|
|
25
30
|
arch = process.arch,
|
|
31
|
+
env = process.env,
|
|
26
32
|
resolve = (specifier) => createRequire(import.meta.url).resolve(specifier),
|
|
27
33
|
exists = existsSync
|
|
28
34
|
}: HelperPathOptions = {}): string | null => {
|
|
35
|
+
const override = env.ADECORE_DATABASE_HELPER;
|
|
36
|
+
|
|
37
|
+
// Not checked for existence: a wrong override fails loudly at spawn instead of quietly running another helper.
|
|
38
|
+
if (override) {
|
|
39
|
+
return override;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const binary = platform === 'win32' ? 'adecore-database.exe' : 'adecore-database';
|
|
43
|
+
|
|
44
|
+
return packagedHelper(platform, arch, binary, resolve, exists) ?? checkoutHelper(binary, exists);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const packagedHelper = (
|
|
48
|
+
platform: string,
|
|
49
|
+
arch: string,
|
|
50
|
+
binary: string,
|
|
51
|
+
resolve: (specifier: string) => string,
|
|
52
|
+
exists: (path: string) => boolean
|
|
53
|
+
): string | null => {
|
|
29
54
|
let manifest: string;
|
|
30
55
|
|
|
31
56
|
try {
|
|
@@ -34,8 +59,21 @@ export const helperPath = ({
|
|
|
34
59
|
return null;
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
const
|
|
38
|
-
const unpacked = binary.replace(`.asar${sep}`, `.asar.unpacked${sep}`);
|
|
62
|
+
const unpacked = join(dirname(manifest), 'bin', binary).replace(`.asar${sep}`, `.asar.unpacked${sep}`);
|
|
39
63
|
|
|
40
64
|
return exists(unpacked) ? unpacked : null;
|
|
41
65
|
};
|
|
66
|
+
|
|
67
|
+
// The published package ships `src` without `helper`, so `Cargo.toml` is what tells a checkout apart from an install.
|
|
68
|
+
const checkoutHelper = (binary: string, exists: (path: string) => boolean): string | null => {
|
|
69
|
+
const here = new URL('.', import.meta.url);
|
|
70
|
+
|
|
71
|
+
if (here.protocol !== 'file:' || !here.pathname.endsWith('/src/host/')) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const helper = new URL('../../helper/', here);
|
|
76
|
+
const built = fileURLToPath(new URL(`target/release/${binary}`, helper));
|
|
77
|
+
|
|
78
|
+
return exists(fileURLToPath(new URL('Cargo.toml', helper))) && exists(built) ? built : null;
|
|
79
|
+
};
|
package/src/host/host.ts
CHANGED
|
@@ -319,6 +319,12 @@ export const createDatabaseHost = (options: DatabaseHostOptions): DatabaseHost =
|
|
|
319
319
|
sessions.delete(request.params.session);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
// The helper ends a session whose connection to the server was lost.
|
|
323
|
+
const target = sessionOf(request);
|
|
324
|
+
if (target !== null && !response.ok && response.error.code === 'unknown-session') {
|
|
325
|
+
sessions.delete(target);
|
|
326
|
+
}
|
|
327
|
+
|
|
322
328
|
if (request.method !== 'open' || !response.ok) {
|
|
323
329
|
return { ...response, id: request.id } as DatabaseResponse;
|
|
324
330
|
}
|
package/src/host/validate.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type ParsedRequest =
|
|
|
7
7
|
const MAX_ID_LENGTH = 200;
|
|
8
8
|
const MAX_LIMIT = 10000;
|
|
9
9
|
const MAX_CELL_LIMIT = 1048576;
|
|
10
|
+
const MAX_BYTES = 67108864;
|
|
10
11
|
const TLS_MODES = ['disable', 'prefer', 'require', 'verify'];
|
|
11
12
|
const TRANSACTION_ACTIONS = ['begin', 'commit', 'rollback'];
|
|
12
13
|
const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
|
|
@@ -250,6 +251,8 @@ const exportSource = (input: unknown, label: string): void => {
|
|
|
250
251
|
|
|
251
252
|
const cellLimit = (params: Dict): void => optional(params, 'cellLimit', 'params', (limit, name) => integer(limit, name, 1, MAX_CELL_LIMIT));
|
|
252
253
|
|
|
254
|
+
const maxBytes = (params: Dict): void => optional(params, 'maxBytes', 'params', (limit, name) => integer(limit, name, 1, MAX_BYTES));
|
|
255
|
+
|
|
253
256
|
const table = (params: Dict): void => {
|
|
254
257
|
filledText(params.session, 'params.session');
|
|
255
258
|
filledText(params.schema, 'params.schema');
|
|
@@ -270,13 +273,14 @@ const PARAMS: { readonly [M in DatabaseMethod]: (input: unknown) => void } = {
|
|
|
270
273
|
},
|
|
271
274
|
structure: (input) => table(shape(input, 'params', TARGET)),
|
|
272
275
|
rows: (input) => {
|
|
273
|
-
const params = shape(input, 'params', [...TARGET, 'offset', 'limit'], ['where', 'orderBy', 'cellLimit']);
|
|
276
|
+
const params = shape(input, 'params', [...TARGET, 'offset', 'limit'], ['where', 'orderBy', 'cellLimit', 'maxBytes']);
|
|
274
277
|
table(params);
|
|
275
278
|
optional(params, 'where', 'params', anyText);
|
|
276
279
|
optional(params, 'orderBy', 'params', anyText);
|
|
277
280
|
integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
|
|
278
281
|
integer(params.limit, 'params.limit', 1, MAX_LIMIT);
|
|
279
282
|
cellLimit(params);
|
|
283
|
+
maxBytes(params);
|
|
280
284
|
},
|
|
281
285
|
count: (input) => {
|
|
282
286
|
const params = shape(input, 'params', TARGET, ['where']);
|
|
@@ -301,21 +305,23 @@ const PARAMS: { readonly [M in DatabaseMethod]: (input: unknown) => void } = {
|
|
|
301
305
|
params.changes.forEach((change, i) => rowChange(change, `params.changes[${i}]`));
|
|
302
306
|
},
|
|
303
307
|
execute: (input) => {
|
|
304
|
-
const params = shape(input, 'params', ['session', 'sql'], ['schema', 'limit', 'cellLimit']);
|
|
308
|
+
const params = shape(input, 'params', ['session', 'sql'], ['schema', 'limit', 'cellLimit', 'maxBytes']);
|
|
305
309
|
filledText(params.session, 'params.session');
|
|
306
310
|
filledText(params.sql, 'params.sql');
|
|
307
311
|
optional(params, 'schema', 'params', filledText);
|
|
308
312
|
optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
|
|
309
313
|
cellLimit(params);
|
|
314
|
+
maxBytes(params);
|
|
310
315
|
},
|
|
311
316
|
page: (input) => {
|
|
312
|
-
const params = shape(input, 'params', ['session', 'sql', 'offset', 'limit'], ['schema', 'cellLimit']);
|
|
317
|
+
const params = shape(input, 'params', ['session', 'sql', 'offset', 'limit'], ['schema', 'cellLimit', 'maxBytes']);
|
|
313
318
|
filledText(params.session, 'params.session');
|
|
314
319
|
filledText(params.sql, 'params.sql');
|
|
315
320
|
optional(params, 'schema', 'params', filledText);
|
|
316
321
|
integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
|
|
317
322
|
integer(params.limit, 'params.limit', 1, MAX_LIMIT);
|
|
318
323
|
cellLimit(params);
|
|
324
|
+
maxBytes(params);
|
|
319
325
|
},
|
|
320
326
|
transaction: (input) => {
|
|
321
327
|
const params = shape(input, 'params', ['session', 'action']);
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export { useDatabaseClient } from './client-context.ts';
|
|
|
34
34
|
export type { NumberNotation } from './grid/display.ts';
|
|
35
35
|
export { QueryConsole, type QueryConsoleProps } from './console/QueryConsole.tsx';
|
|
36
36
|
export type { QueryConsoleEditorHandle, QueryConsoleEditorProps, QueryConsoleRunScope } from './console/editor-slot.ts';
|
|
37
|
+
export { RUN_ALL_SHORTCUT, RUN_SHORTCUT, runScopeOf } from './console/shortcuts.ts';
|
|
37
38
|
export { StructureView, type StructureViewProps } from './structure/StructureView.tsx';
|
|
38
39
|
export { TableView, type TableViewProps } from './table/TableView.tsx';
|
|
39
40
|
export { DatabaseWorkbench, type DatabaseWorkbenchProps } from './workbench/DatabaseWorkbench.tsx';
|