@adecore/database 0.23.1 → 0.24.1

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.
Files changed (58) hide show
  1. package/README.md +2 -2
  2. package/dist/client/client.js +31 -11
  3. package/dist/client/types.d.ts +3 -0
  4. package/dist/console/QueryConsole.js +8 -10
  5. package/dist/console/SqlEditor.js +5 -10
  6. package/dist/console/StatementResultView.d.ts +3 -1
  7. package/dist/console/StatementResultView.js +5 -2
  8. package/dist/console/shortcuts.d.ts +3 -0
  9. package/dist/console/shortcuts.js +14 -3
  10. package/dist/grid/DataGrid.d.ts +3 -1
  11. package/dist/grid/DataGrid.js +35 -22
  12. package/dist/grid/GridRowView.js +2 -2
  13. package/dist/grid/RecordView.js +2 -2
  14. package/dist/grid/display.d.ts +2 -0
  15. package/dist/grid/display.js +11 -0
  16. package/dist/grid/whole-cells.d.ts +9 -0
  17. package/dist/grid/whole-cells.js +49 -0
  18. package/dist/host/helper-path.d.ts +2 -1
  19. package/dist/host/helper-path.js +26 -5
  20. package/dist/host/host.js +5 -0
  21. package/dist/host/validate.js +8 -3
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/locales/en.d.ts +601 -0
  25. package/dist/locales/en.js +600 -0
  26. package/dist/locales/nl.d.ts +601 -0
  27. package/dist/locales/nl.js +600 -0
  28. package/dist/locales.js +3 -2
  29. package/dist/protocol/messages.d.ts +4 -1
  30. package/dist/protocol/messages.js +1 -1
  31. package/dist/table/TableView.js +1 -1
  32. package/dist/testing/fake.js +24 -14
  33. package/package.json +8 -8
  34. package/src/client/client.ts +32 -11
  35. package/src/client/types.ts +10 -2
  36. package/src/console/QueryConsole.tsx +10 -9
  37. package/src/console/SqlEditor.tsx +5 -10
  38. package/src/console/StatementResultView.tsx +15 -1
  39. package/src/console/shortcuts.ts +16 -3
  40. package/src/grid/DataGrid.tsx +47 -24
  41. package/src/grid/GridRowView.tsx +2 -2
  42. package/src/grid/RecordView.tsx +2 -2
  43. package/src/grid/display.ts +13 -0
  44. package/src/grid/whole-cells.ts +72 -0
  45. package/src/host/helper-path.ts +42 -4
  46. package/src/host/host.ts +6 -0
  47. package/src/host/validate.ts +9 -3
  48. package/src/index.ts +1 -0
  49. package/src/locales/en.ts +600 -0
  50. package/src/locales/nl.ts +600 -0
  51. package/src/locales.ts +3 -2
  52. package/src/protocol/messages.ts +21 -4
  53. package/src/table/TableView.tsx +2 -1
  54. package/src/testing/fake.ts +29 -14
  55. package/dist/locales/en.json +0 -594
  56. package/dist/locales/nl.json +0 -594
  57. package/src/locales/en.json +0 -594
  58. package/src/locales/nl.json +0 -594
@@ -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
+ };
@@ -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 prebuilt helper that npm installed for this machine, in the `@adecore/database-<platform>-<arch>`
17
- * package, or null when there is none: an unsupported platform, or an install with optional dependencies off.
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 binary = join(dirname(manifest), 'bin', platform === 'win32' ? 'adecore-database.exe' : 'adecore-database');
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
  }
@@ -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';