@adecore/database 0.39.0 → 0.40.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 (131) hide show
  1. package/dist/console/QueryConsole.js +2 -10
  2. package/dist/console/SchemaPicker.js +0 -1
  3. package/dist/console/StatementResultView.js +1 -1
  4. package/dist/ddl/diff.js +1 -1
  5. package/dist/explorer/tree.js +0 -1
  6. package/dist/export-formats.d.ts +2 -0
  7. package/dist/export-formats.js +1 -0
  8. package/dist/grid/CopyAsMenu.js +0 -1
  9. package/dist/grid/DataGrid.js +10 -10
  10. package/dist/grid/column-layout.d.ts +0 -1
  11. package/dist/grid/column-layout.js +1 -2
  12. package/dist/grid/column-view.js +3 -2
  13. package/dist/grid/display.js +1 -2
  14. package/dist/host/host.js +1 -2
  15. package/dist/host/validate.d.ts +3 -0
  16. package/dist/host/validate.js +18 -15
  17. package/dist/same-list.d.ts +1 -0
  18. package/dist/same-list.js +1 -0
  19. package/dist/stored-size.d.ts +1 -0
  20. package/dist/stored-size.js +10 -0
  21. package/dist/table/TableToolbar.js +1 -1
  22. package/dist/table/TableView.js +2 -10
  23. package/package.json +15 -15
  24. package/src/DatabaseProvider.tsx +2 -0
  25. package/src/KeyIcon.tsx +1 -0
  26. package/src/client/client.ts +13 -0
  27. package/src/client-context.ts +2 -0
  28. package/src/connections/ConnectionForm.tsx +11 -0
  29. package/src/connections/ConnectionManager.tsx +9 -0
  30. package/src/connections/connection-config.ts +21 -0
  31. package/src/connections/docker.ts +2 -0
  32. package/src/connections/use-docker-containers.ts +3 -0
  33. package/src/console/DestructiveDialog.tsx +1 -0
  34. package/src/console/QueryConsole.tsx +38 -10
  35. package/src/console/SchemaPicker.tsx +1 -1
  36. package/src/console/SqlEditor.tsx +10 -0
  37. package/src/console/StatementResultView.tsx +7 -2
  38. package/src/console/TransactionControls.tsx +1 -0
  39. package/src/console/destructive.ts +11 -0
  40. package/src/console/editing.ts +10 -0
  41. package/src/console/history.ts +8 -0
  42. package/src/console/labels.ts +1 -0
  43. package/src/console/outcome.ts +1 -0
  44. package/src/console/result-keys.ts +16 -0
  45. package/src/console/shortcuts.ts +2 -0
  46. package/src/ddl/alter-mysql.ts +16 -0
  47. package/src/ddl/alter-sqlite.ts +21 -0
  48. package/src/ddl/alter.ts +2 -0
  49. package/src/ddl/column.ts +11 -0
  50. package/src/ddl/create.ts +15 -0
  51. package/src/ddl/dialect.ts +4 -0
  52. package/src/ddl/diff.ts +22 -2
  53. package/src/ddl/draft.ts +1 -0
  54. package/src/ddl/parse.ts +31 -0
  55. package/src/ddl/types.ts +3 -0
  56. package/src/ddl/validate.ts +12 -0
  57. package/src/designer/ColumnsEditor.tsx +3 -0
  58. package/src/designer/DesignerView.tsx +2 -0
  59. package/src/designer/ForeignKeysEditor.tsx +4 -0
  60. package/src/designer/IndexesEditor.tsx +1 -0
  61. package/src/designer/NamePicker.tsx +2 -0
  62. package/src/designer/OptionsEditor.tsx +4 -0
  63. package/src/designer/SqlPreview.tsx +2 -0
  64. package/src/designer/TableDesigner.tsx +14 -0
  65. package/src/designer/edit.ts +13 -0
  66. package/src/designer/plan.ts +3 -0
  67. package/src/designer/recover.ts +1 -0
  68. package/src/designer/useReferenceColumns.ts +5 -0
  69. package/src/explorer/DatabaseExplorer.tsx +40 -0
  70. package/src/explorer/RowMenu.tsx +2 -0
  71. package/src/explorer/TableDialog.tsx +6 -0
  72. package/src/explorer/expansion.ts +8 -0
  73. package/src/explorer/tree.ts +73 -2
  74. package/src/explorer/use-explorer-loads.ts +15 -0
  75. package/src/explorer/use-stored-expansion.ts +4 -0
  76. package/src/export-formats.ts +3 -0
  77. package/src/grid/CellEditor.tsx +1 -0
  78. package/src/grid/CopyAsMenu.tsx +1 -1
  79. package/src/grid/DataGrid.tsx +93 -23
  80. package/src/grid/EnumPicker.tsx +6 -0
  81. package/src/grid/GridHeaderCell.tsx +11 -0
  82. package/src/grid/GridRowView.tsx +3 -0
  83. package/src/grid/RecordView.tsx +19 -0
  84. package/src/grid/ValueDock.tsx +2 -0
  85. package/src/grid/aggregates.ts +12 -0
  86. package/src/grid/column-layout.ts +12 -4
  87. package/src/grid/column-selection.ts +9 -0
  88. package/src/grid/column-view.ts +6 -2
  89. package/src/grid/copy-formats.ts +13 -0
  90. package/src/grid/display.ts +17 -3
  91. package/src/grid/edit-value.ts +11 -0
  92. package/src/grid/enum-type.ts +15 -0
  93. package/src/grid/focused-value.ts +4 -0
  94. package/src/grid/layout.ts +12 -0
  95. package/src/grid/navigation.ts +5 -0
  96. package/src/grid/row-selection.ts +7 -0
  97. package/src/grid/sort.ts +1 -0
  98. package/src/grid/whole-cells.ts +8 -0
  99. package/src/host/host.ts +16 -3
  100. package/src/host/spawn.ts +2 -0
  101. package/src/host/validate.ts +37 -15
  102. package/src/same-list.ts +2 -0
  103. package/src/sql-split.ts +28 -0
  104. package/src/sql.ts +20 -0
  105. package/src/stored-size.ts +10 -0
  106. package/src/structure/StructureView.tsx +18 -0
  107. package/src/table/CommandField.tsx +11 -0
  108. package/src/table/ImportDialog.tsx +4 -0
  109. package/src/table/TableStatusBar.tsx +1 -0
  110. package/src/table/TableToolbar.tsx +2 -2
  111. package/src/table/TableView.tsx +50 -10
  112. package/src/table/command-field.ts +40 -0
  113. package/src/table/grid-rows.ts +13 -0
  114. package/src/table/import-mapping.ts +5 -0
  115. package/src/table/layout-store.ts +7 -0
  116. package/src/table/paging.ts +1 -0
  117. package/src/table/pending.ts +18 -0
  118. package/src/table/references.ts +4 -0
  119. package/src/table/useLoaded.ts +3 -0
  120. package/src/table/useStoredLayout.ts +2 -0
  121. package/src/table/useTableTransfer.ts +17 -1
  122. package/src/testing/fake.ts +24 -0
  123. package/src/use-database-resources.ts +1 -0
  124. package/src/use-popup-press.ts +3 -0
  125. package/src/value/ValueEditor.tsx +6 -0
  126. package/src/value/draft.ts +7 -0
  127. package/src/value/hex.ts +8 -0
  128. package/src/value/json.ts +16 -0
  129. package/src/value/views.ts +14 -0
  130. package/src/workbench/DatabaseWorkbench.tsx +22 -0
  131. package/src/workbench/tabs.ts +34 -0
@@ -30,9 +30,11 @@ export const valuesEqual = (left: EditValue | undefined, right: EditValue | unde
30
30
  if (left === undefined || right === undefined) {
31
31
  return false;
32
32
  }
33
+
33
34
  if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
34
35
  return left === right;
35
36
  }
37
+
36
38
  return left.kind === right.kind && (left.kind !== 'binary' || (right.kind === 'binary' && left.hex === right.hex));
37
39
  };
38
40
 
@@ -41,6 +43,7 @@ export const setEdit = (pending: PendingChanges, row: number, column: string, va
41
43
  const { [row]: current = {}, ...others } = pending.edits;
42
44
  const { [column]: _replaced, ...kept } = current;
43
45
  const next = valuesEqual(value, valueOfCell(original)) ? kept : { ...kept, [column]: value };
46
+
44
47
  return { ...pending, edits: Object.keys(next).length === 0 ? others : { ...others, [row]: next } };
45
48
  };
46
49
 
@@ -69,6 +72,7 @@ export const revertRow = (pending: PendingChanges, row: number): PendingChanges
69
72
  const { [row]: _dropped, ...edits } = pending.edits;
70
73
  const deletes = new Set(pending.deletes);
71
74
  deletes.delete(row);
75
+
72
76
  return { ...pending, edits, deletes };
73
77
  };
74
78
 
@@ -81,6 +85,7 @@ export interface RowSelectionRefs {
81
85
  /* Takes back every change to the given rows: the edits and deletion marks of the loaded ones, and the inserted ones altogether. */
82
86
  export const revertRows = (pending: PendingChanges, rows: RowSelectionRefs): PendingChanges => {
83
87
  const reverted = rows.loaded.reduce(revertRow, pending);
88
+
84
89
  return { ...reverted, inserts: reverted.inserts.filter((row) => !rows.inserted.includes(row.id)) };
85
90
  };
86
91
 
@@ -97,18 +102,24 @@ type LoadedRows = Pick<RowsResult, 'columns' | 'rows'>;
97
102
  */
98
103
  export const rowKeyOf = (structure: TableStructure, loaded: LoadedRows, row: number): RowKey | null => {
99
104
  const cells = loaded.rows[row];
105
+
100
106
  if (structure.rowKey === null || cells === undefined) {
101
107
  return null;
102
108
  }
109
+
103
110
  const key: Record<string, Value> = {};
111
+
104
112
  for (const name of structure.rowKey) {
105
113
  const index = loaded.columns.findIndex((column) => column.name === name);
106
114
  const value = index < 0 ? undefined : valueOfCell(cells[index] as Cell);
115
+
107
116
  if (value === undefined) {
108
117
  return null;
109
118
  }
119
+
110
120
  key[name] = value;
111
121
  }
122
+
112
123
  return key;
113
124
  };
114
125
 
@@ -116,18 +127,23 @@ export const rowKeyOf = (structure: TableStructure, loaded: LoadedRows, row: num
116
127
  export const toRowChanges = (structure: TableStructure, loaded: LoadedRows, pending: PendingChanges): RowChange[] => {
117
128
  const keyOf = (row: number): RowKey => {
118
129
  const key = rowKeyOf(structure, loaded, row);
130
+
119
131
  if (key === null) {
120
132
  throw new Error(`Row ${row + 1} of ${structure.name} has no usable key.`);
121
133
  }
134
+
122
135
  return key;
123
136
  };
137
+
124
138
  const updates = Object.keys(pending.edits)
125
139
  .map(Number)
126
140
  .filter((row) => !pending.deletes.has(row))
127
141
  .sort((left, right) => left - right)
128
142
  .map((row): RowChange => ({ kind: 'update', key: keyOf(row), values: pending.edits[row]! }));
143
+
129
144
  const deletes = [...pending.deletes].sort((left, right) => left - right).map((row): RowChange => ({ kind: 'delete', key: keyOf(row) }));
130
145
  const inserts = pending.inserts.map((row): RowChange => ({ kind: 'insert', values: row.values }));
146
+
131
147
  return [...updates, ...deletes, ...inserts];
132
148
  };
133
149
 
@@ -135,9 +151,11 @@ const keyValueText = (value: Value): string => {
135
151
  if (value === null) {
136
152
  return 'NULL';
137
153
  }
154
+
138
155
  if (typeof value === 'object') {
139
156
  return `0x${value.hex}`;
140
157
  }
158
+
141
159
  return typeof value === 'string' ? `'${value}'` : String(value);
142
160
  };
143
161
 
@@ -25,14 +25,18 @@ export const referenceOf = (
25
25
  cells: readonly Shown[]
26
26
  ): Reference | null => {
27
27
  const matches: ColumnMatch[] = [];
28
+
28
29
  for (const [position, name] of key.columns.entries()) {
29
30
  const referenced = key.referencedColumns[position];
30
31
  const index = columns.findIndex((column) => column.name === name);
31
32
  const value = index < 0 ? undefined : wholeValueOf(cells[index] ?? null);
33
+
32
34
  if (referenced === undefined || value === undefined || value === null) {
33
35
  return null;
34
36
  }
37
+
35
38
  matches.push({ column: referenced, value, kind: columns[index]!.kind });
36
39
  }
40
+
37
41
  return matches.length === 0 ? null : { schema: key.referencedSchema, table: key.referencedTable, where: matchCondition(engine, matches) };
38
42
  };
@@ -33,6 +33,7 @@ export function useLoaded<T>(load: (signal: AbortSignal) => Promise<T>, key: str
33
33
 
34
34
  useEffect(() => {
35
35
  const controller = new AbortController();
36
+
36
37
  loader.current(controller.signal).then(
37
38
  (value) => {
38
39
  if (!controller.signal.aborted) {
@@ -45,10 +46,12 @@ export function useLoaded<T>(load: (signal: AbortSignal) => Promise<T>, key: str
45
46
  }
46
47
  }
47
48
  );
49
+
48
50
  return () => controller.abort();
49
51
  }, [key, version]);
50
52
 
51
53
  const answered = state.key === key && state.version === version;
54
+
52
55
  return {
53
56
  value: state.value,
54
57
  loading: !answered,
@@ -30,6 +30,7 @@ export function useStoredLayout(storage: DatabaseStorage | undefined, key: strin
30
30
  const write = useCallback((): void => {
31
31
  clearTimeout(timer.current);
32
32
  timer.current = undefined;
33
+
33
34
  if (dirty.current) {
34
35
  dirty.current = false;
35
36
  target.current.storage?.set(target.current.key, serializeLayout(latest.current));
@@ -44,6 +45,7 @@ export function useStoredLayout(storage: DatabaseStorage | undefined, key: strin
44
45
  if (storage === undefined) {
45
46
  return;
46
47
  }
48
+
47
49
  latest.current = { ...latest.current, ...change };
48
50
  dirty.current = true;
49
51
  clearTimeout(timer.current);
@@ -49,7 +49,6 @@ export interface TableTransfer {
49
49
  readonly notice: TransferNotice | null;
50
50
  readonly importDraft: ImportDraft | null;
51
51
  exportAs(format: FileFormat): void;
52
- /* Stops the export that is running. */
53
52
  cancelExport(): void;
54
53
  dismiss(): void;
55
54
  chooseImportFile(): void;
@@ -71,7 +70,9 @@ export function useTableTransfer({ client, session, files, schema, table, struct
71
70
  if (notice?.kind !== 'exported' && notice?.kind !== 'imported') {
72
71
  return;
73
72
  }
73
+
74
74
  const timer = setTimeout(() => setNotice(null), NOTICE_MS);
75
+
75
76
  return () => clearTimeout(timer);
76
77
  }, [notice]);
77
78
 
@@ -90,19 +91,24 @@ export function useTableTransfer({ client, session, files, schema, table, struct
90
91
  if (files === undefined || exporting.current !== null) {
91
92
  return;
92
93
  }
94
+
93
95
  void (async () => {
94
96
  const path = await files.save({ suggestedName: `${table}.${format}`, format });
97
+
95
98
  if (path === null) {
96
99
  return;
97
100
  }
101
+
98
102
  const controller = new AbortController();
99
103
  exporting.current = controller;
100
104
  setNotice({ kind: 'exporting', format });
105
+
101
106
  try {
102
107
  const result = await session.export(
103
108
  { source: { kind: 'table', schema, table, where: where || undefined, orderBy: orderBy || undefined }, format, path },
104
109
  { signal: controller.signal }
105
110
  );
111
+
106
112
  show({ kind: 'exported', rows: result.rows });
107
113
  } catch (error) {
108
114
  if (controller.signal.aborted || isCancelled(error)) {
@@ -120,6 +126,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
120
126
  sampling.current?.abort();
121
127
  const controller = new AbortController();
122
128
  sampling.current = controller;
129
+
123
130
  return await client.sample(path, format, header, { signal: controller.signal });
124
131
  };
125
132
 
@@ -127,12 +134,16 @@ export function useTableTransfer({ client, session, files, schema, table, struct
127
134
  if (files === undefined || structure === null) {
128
135
  return;
129
136
  }
137
+
130
138
  void (async () => {
131
139
  const path = await files.open({ formats: ['csv', 'tsv'] });
140
+
132
141
  if (path === null) {
133
142
  return;
134
143
  }
144
+
135
145
  const format = formatOfPath(path);
146
+
136
147
  try {
137
148
  const read = await sample(path, format, true);
138
149
  setImportDraft({ path, format, header: true, sample: read, mapping: matchColumns(read.columns, tableColumns, true), busy: false, error: null });
@@ -146,11 +157,14 @@ export function useTableTransfer({ client, session, files, schema, table, struct
146
157
  if (importDraft === null) {
147
158
  return;
148
159
  }
160
+
149
161
  const { path, format } = importDraft;
150
162
  setImportDraft({ ...importDraft, header, sample: null, error: null });
163
+
151
164
  void (async () => {
152
165
  try {
153
166
  const read = await sample(path, format, header);
167
+
154
168
  setImportDraft((now) =>
155
169
  now === null || now.path !== path ? now : { ...now, header, sample: read, mapping: matchColumns(read.columns, tableColumns, header) }
156
170
  );
@@ -170,8 +184,10 @@ export function useTableTransfer({ client, session, files, schema, table, struct
170
184
  if (importDraft === null || importDraft.busy || importDraft.sample === null) {
171
185
  return;
172
186
  }
187
+
173
188
  const { path, format, header, mapping } = importDraft;
174
189
  setImportDraft({ ...importDraft, busy: true, error: null });
190
+
175
191
  void (async () => {
176
192
  try {
177
193
  const rows = await session.import(schema, table, { path, format, header, columns: mapping });
@@ -149,6 +149,7 @@ const rowKeyOf = (table: Table): string[] | null => {
149
149
  const unique = table.indexes.find(
150
150
  (index) => index.unique && index.columns.every((name) => table.columns.find((column) => column.name === name)?.nullable === false)
151
151
  );
152
+
152
153
  return unique === undefined ? null : [...unique.columns];
153
154
  };
154
155
 
@@ -180,6 +181,7 @@ const toCell = (value: Value, cellLimit: number): Cell => {
180
181
 
181
182
  if (value !== null && typeof value === 'object') {
182
183
  const length = value.hex.length / 2;
184
+
183
185
  return { kind: 'binary', hex: value.hex.slice(0, Math.min(length, cellLimit) * 2), length };
184
186
  }
185
187
 
@@ -196,16 +198,20 @@ interface ByteBudget {
196
198
 
197
199
  const toRowsResult = (table: Table, rows: readonly (readonly Value[])[], offset: number, limit: number, cellLimit: number, budget: ByteBudget): RowsResult => {
198
200
  const page: Cell[][] = [];
201
+
199
202
  for (const row of rows.slice(offset, offset + limit)) {
200
203
  const cells = row.map((value) => toCell(value, cellLimit));
201
204
  // Characters rather than bytes: near enough for a fake.
202
205
  const size = JSON.stringify(cells).length;
206
+
203
207
  if (page.length > 0 && size > budget.left) {
204
208
  return { columns: toResultColumns(table), rows: page, hasMore: true, elapsedMs: 0 };
205
209
  }
210
+
206
211
  budget.left = Math.max(0, budget.left - size);
207
212
  page.push(cells);
208
213
  }
214
+
209
215
  return { columns: toResultColumns(table), rows: page, hasMore: rows.length > offset + limit, elapsedMs: 0 };
210
216
  };
211
217
 
@@ -213,6 +219,7 @@ const toRowsResult = (table: Table, rows: readonly (readonly Value[])[], offset:
213
219
  const defaultOf = (column: ColumnInfo, table: Table): Value => {
214
220
  if (column.autoIncrement) {
215
221
  const index = table.columns.indexOf(column);
222
+
216
223
  return table.rows.reduce((highest, row) => (typeof row[index] === 'number' ? Math.max(highest, row[index]) : highest), 0) + 1;
217
224
  }
218
225
 
@@ -272,6 +279,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
272
279
 
273
280
  const row = table.columns.map((column) => {
274
281
  const given = Object.hasOwn(change.values, column.name) ? change.values[column.name]! : { kind: 'default' as const };
282
+
275
283
  return resolveEdit(column, draft, given);
276
284
  });
277
285
 
@@ -285,6 +293,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
285
293
  }
286
294
 
287
295
  draft.rows.push(row);
296
+
288
297
  return;
289
298
  }
290
299
 
@@ -302,6 +311,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
302
311
 
303
312
  if (change.kind === 'delete') {
304
313
  draft.rows.splice(draft.rows.indexOf(row), 1);
314
+
305
315
  return;
306
316
  }
307
317
 
@@ -312,6 +322,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
312
322
  });
313
323
 
314
324
  table.rows = draft.rows;
325
+
315
326
  return changes.length;
316
327
  };
317
328
 
@@ -331,6 +342,7 @@ const selectedTable = (session: Session, sql: string): Table => {
331
342
  const runStatement = (session: Session, sql: string, limit: number, cellLimit: number, budget: ByteBudget): StatementResult => {
332
343
  try {
333
344
  const table = selectedTable(session, sql);
345
+
334
346
  return { kind: 'rows', sql, ...toRowsResult(table, table.rows, 0, limit, cellLimit, budget) };
335
347
  } catch (error) {
336
348
  if (error instanceof FakeError) {
@@ -402,15 +414,18 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
402
414
  const session = connect(connection);
403
415
  const id = `fake-${++counter}`;
404
416
  sessions.set(id, session);
417
+
405
418
  return { session: id, server: serverOf(connection) };
406
419
  },
407
420
  close: ({ session }) => {
408
421
  sessionOf(session);
409
422
  sessions.delete(session);
423
+
410
424
  return null;
411
425
  },
412
426
  test: ({ connection }) => {
413
427
  connect(connection);
428
+
414
429
  return { server: serverOf(connection) };
415
430
  },
416
431
  schemas: ({ session }) => ({
@@ -418,10 +433,12 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
418
433
  }),
419
434
  tables: ({ session, schema }) => {
420
435
  const tables = findSchema(sessionOf(session), schema);
436
+
421
437
  return { tables: [...tables].map(([name, table]) => ({ name, kind: table.kind, rowEstimate: table.rows.length, comment: null })) };
422
438
  },
423
439
  structure: ({ session, schema, table: name }) => {
424
440
  const table = findTable(sessionOf(session), schema, name);
441
+
425
442
  return {
426
443
  schema,
427
444
  name,
@@ -438,6 +455,7 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
438
455
  },
439
456
  rows: ({ session, schema, table: name, offset, limit, cellLimit, maxBytes }) => {
440
457
  const table = findTable(sessionOf(session), schema, name);
458
+
441
459
  return toRowsResult(table, table.rows, offset, limit, cellLimit ?? DEFAULT_ROWS_CELL_LIMIT, { left: maxBytes ?? DEFAULT_MAX_BYTES });
442
460
  },
443
461
  count: ({ session, schema, table: name }) => ({ count: findTable(sessionOf(session), schema, name).rows.length }),
@@ -491,15 +509,18 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
491
509
  const target = sessionOf(session);
492
510
  selectSchema(target, schema);
493
511
  const table = selectedTable(target, sql);
512
+
494
513
  return toRowsResult(table, table.rows, offset, limit, cellLimit ?? DEFAULT_EXECUTE_CELL_LIMIT, { left: maxBytes ?? DEFAULT_MAX_BYTES });
495
514
  },
496
515
  transaction: ({ session, action }) => {
497
516
  const target = sessionOf(session);
498
517
  target.inTransaction = action === 'begin';
518
+
499
519
  return { active: target.inTransaction };
500
520
  },
501
521
  export: ({ session }) => {
502
522
  sessionOf(session);
523
+
503
524
  throw new FakeError('unsupported', 'The fake database has no files to export to.');
504
525
  },
505
526
  sample: () => {
@@ -507,12 +528,14 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
507
528
  },
508
529
  import: ({ session }) => {
509
530
  sessionOf(session);
531
+
510
532
  throw new FakeError('unsupported', 'The fake database has no files to import from.');
511
533
  },
512
534
  discover: () => ({ containers: structuredClone([...(options.containers ?? [])]) }),
513
535
  cancel: ({ request }) => {
514
536
  const stop = waiting.get(request);
515
537
  stop?.();
538
+
516
539
  return { cancelled: stop !== undefined };
517
540
  }
518
541
  };
@@ -520,6 +543,7 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
520
543
  const answer = (request: DatabaseRequest): DatabaseResponse => {
521
544
  try {
522
545
  const handler = handlers[request.method] as (params: unknown) => unknown;
546
+
523
547
  return { id: request.id, ok: true, result: handler(request.params) } as DatabaseResponse;
524
548
  } catch (error) {
525
549
  if (error instanceof FakeError) {
@@ -7,6 +7,7 @@ import { addDatabaseResources } from './locales.ts';
7
7
  export function useDatabaseResources(): void {
8
8
  const { i18n } = useTranslation();
9
9
  const [added, setAdded] = useState<I18n | null>(null);
10
+
10
11
  if (added !== i18n) {
11
12
  addDatabaseResources(i18n);
12
13
  setAdded(i18n);
@@ -13,7 +13,9 @@ export function usePopupPress(): () => boolean {
13
13
  const press = (e: PointerEvent): void => {
14
14
  pressed.current = isInFloatingLayer(e.target);
15
15
  };
16
+
16
17
  document.addEventListener('pointerdown', press, true);
18
+
17
19
  return () => {
18
20
  document.removeEventListener('pointerdown', press, true);
19
21
  };
@@ -22,6 +24,7 @@ export function usePopupPress(): () => boolean {
22
24
  return () => {
23
25
  const was = pressed.current;
24
26
  pressed.current = false;
27
+
25
28
  return was;
26
29
  };
27
30
  }
@@ -75,7 +75,9 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
75
75
  if (!editable || onCommit === undefined || !dirty) {
76
76
  return;
77
77
  }
78
+
78
79
  const conversion = editValueOf(draft, source);
80
+
79
81
  if (conversion.ok) {
80
82
  onCommit(conversion.value);
81
83
  } else {
@@ -123,9 +125,11 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
123
125
  if (content.kind === 'empty') {
124
126
  return <Faint>{placeholder}</Faint>;
125
127
  }
128
+
126
129
  if (content.kind === 'problem') {
127
130
  return <Faint>{problemText(content.problem)}</Faint>;
128
131
  }
132
+
129
133
  return (
130
134
  <>
131
135
  <pre className={clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre')}>{content.text}</pre>
@@ -142,6 +146,7 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
142
146
  if (view === 'text') {
143
147
  return editor('min-h-0 flex-1');
144
148
  }
149
+
145
150
  if (view === 'hex' && editable) {
146
151
  return (
147
152
  <>
@@ -150,6 +155,7 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
150
155
  </>
151
156
  );
152
157
  }
158
+
153
159
  return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
154
160
  };
155
161
 
@@ -30,6 +30,7 @@ const textOf = (value: Exclude<Value, null>): string => (typeof value === 'objec
30
30
  export const sourceOf = (column: Pick<GridColumn, 'name' | 'type' | 'kind'>, value: Value): Source => {
31
31
  const binary = column.kind === 'binary' || (value !== null && typeof value === 'object');
32
32
  const original: Draft = value === null ? { text: '', mode: 'null' } : { text: textOf(value), mode: 'value' };
33
+
33
34
  return {
34
35
  name: column.name,
35
36
  type: column.type,
@@ -50,18 +51,24 @@ export const editValueOf = (draft: Draft, source: Source): Conversion => {
50
51
  if (draft.mode === 'null') {
51
52
  return { ok: true, value: null };
52
53
  }
54
+
53
55
  if (draft.mode === 'default') {
54
56
  return { ok: true, value: { kind: 'default' } };
55
57
  }
58
+
56
59
  if (source.binary) {
57
60
  const parsed = parseHex(draft.text);
61
+
58
62
  return parsed.ok ? { ok: true, value: { kind: 'binary', hex: parsed.hex } } : { ok: false, problem: { kind: 'hex', problem: parsed.problem } };
59
63
  }
64
+
60
65
  if (source.json) {
61
66
  const checked = checkJson(draft.text);
67
+
62
68
  if (!checked.ok) {
63
69
  return { ok: false, problem: { kind: 'json', reason: checked.reason } };
64
70
  }
65
71
  }
72
+
66
73
  return { ok: true, value: parseDraft(draft.text, source.kind) };
67
74
  };
package/src/value/hex.ts CHANGED
@@ -15,17 +15,21 @@ const UUID_BYTES = 16;
15
15
  export const parseHex = (text: string): ParsedHex => {
16
16
  const stripped = text.replace(/\s+/g, '').toLowerCase();
17
17
  const hex = stripped.startsWith('0x') ? stripped.slice(2) : stripped;
18
+
18
19
  if (NOT_HEX.test(hex)) {
19
20
  return { ok: false, problem: 'characters' };
20
21
  }
22
+
21
23
  return hex.length % 2 === 0 ? { ok: true, hex } : { ok: false, problem: 'odd' };
22
24
  };
23
25
 
24
26
  export const bytesOf = (hex: string): Uint8Array => {
25
27
  const bytes = new Uint8Array(hex.length / 2);
28
+
26
29
  for (let i = 0; i < bytes.length; i++) {
27
30
  bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
28
31
  }
32
+
29
33
  return bytes;
30
34
  };
31
35
 
@@ -37,6 +41,7 @@ export const formatUuid = (hex: string): string | null => {
37
41
  if (hex.length !== UUID_BYTES * 2) {
38
42
  return null;
39
43
  }
44
+
40
45
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
41
46
  };
42
47
 
@@ -51,12 +56,15 @@ const slotsOf = (bytes: Uint8Array, from: number, to: number): string =>
51
56
  export const hexDumpLines = (hex: string, maxBytes = Number.POSITIVE_INFINITY): string[] => {
52
57
  const bytes = bytesOf(hex.slice(0, Math.min(hex.length, maxBytes * 2)));
53
58
  const lines: string[] = [];
59
+
54
60
  for (let offset = 0; offset < bytes.length; offset += BYTES_PER_LINE) {
55
61
  const line = bytes.subarray(offset, offset + BYTES_PER_LINE);
56
62
  const ascii = Array.from(line, (byte) => (isPrintable(byte) ? String.fromCharCode(byte) : '.')).join('');
63
+
57
64
  lines.push(
58
65
  `${offset.toString(16).padStart(8, '0')} ${slotsOf(bytes, offset, offset + GROUP)} ${slotsOf(bytes, offset + GROUP, offset + BYTES_PER_LINE)} |${ascii}|`
59
66
  );
60
67
  }
68
+
61
69
  return lines;
62
70
  };
package/src/value/json.ts CHANGED
@@ -7,6 +7,7 @@ export type JsonCheck = { readonly ok: true } | { readonly ok: false; readonly r
7
7
  export const checkJson = (text: string): JsonCheck => {
8
8
  try {
9
9
  JSON.parse(text);
10
+
10
11
  return { ok: true };
11
12
  } catch (error) {
12
13
  return { ok: false, reason: error instanceof Error ? error.message : String(error) };
@@ -16,6 +17,7 @@ export const checkJson = (text: string): JsonCheck => {
16
17
  /* Whether a string is a JSON object or array. A scalar such as `42` or `"x"` is a plain value, not a document. */
17
18
  export const isJsonDocument = (text: string): boolean => {
18
19
  const start = text.trimStart()[0];
20
+
19
21
  return (start === '{' || start === '[') && checkJson(text).ok;
20
22
  };
21
23
 
@@ -28,34 +30,46 @@ export const prettyJson = (text: string): string | null => {
28
30
  let output = '';
29
31
  let depth = 0;
30
32
  let i = 0;
33
+
31
34
  const lineBreak = () => `\n${INDENT.repeat(depth)}`;
35
+
32
36
  const nextToken = (from: number): string => {
33
37
  let at = from;
38
+
34
39
  while (/\s/.test(text[at] ?? '')) {
35
40
  at++;
36
41
  }
42
+
37
43
  return text[at] ?? '';
38
44
  };
39
45
 
40
46
  while (i < text.length) {
41
47
  const char = text[i]!;
48
+
42
49
  if (char === '"') {
43
50
  let end = i + 1;
51
+
44
52
  while (text[end] !== '"') {
45
53
  end += text[end] === '\\' ? 2 : 1;
46
54
  }
55
+
47
56
  output += text.slice(i, end + 1);
48
57
  i = end + 1;
58
+
49
59
  continue;
50
60
  }
61
+
51
62
  if (char === '{' || char === '[') {
52
63
  const close = char === '{' ? '}' : ']';
53
64
  output += char;
65
+
54
66
  if (nextToken(i + 1) === close) {
55
67
  output += close;
56
68
  i = text.indexOf(close, i + 1) + 1;
69
+
57
70
  continue;
58
71
  }
72
+
59
73
  depth++;
60
74
  output += lineBreak();
61
75
  } else if (char === '}' || char === ']') {
@@ -68,7 +82,9 @@ export const prettyJson = (text: string): string | null => {
68
82
  } else if (!/\s/.test(char)) {
69
83
  output += char;
70
84
  }
85
+
71
86
  i++;
72
87
  }
88
+
73
89
  return output.trim();
74
90
  };
@@ -9,8 +9,10 @@ export const viewsOf = (source: Source, draft: Draft): ViewId[] => {
9
9
  if (source.binary) {
10
10
  const parsed = parseHex(draft.text);
11
11
  const hex = parsed.ok ? parsed.hex : source.original.text;
12
+
12
13
  return formatUuid(hex) === null ? ['hex', 'utf8'] : ['hex', 'utf8', 'uuid'];
13
14
  }
15
+
14
16
  return source.json ? ['formatted', 'text'] : ['text'];
15
17
  };
16
18
 
@@ -19,10 +21,13 @@ export const sizeOf = (source: Source, draft: Draft): { unit: 'bytes' | 'charact
19
21
  if (draft.mode !== 'value') {
20
22
  return null;
21
23
  }
24
+
22
25
  if (!source.binary) {
23
26
  return { unit: 'characters', count: draft.text.length };
24
27
  }
28
+
25
29
  const parsed = parseHex(draft.text);
30
+
26
31
  return parsed.ok ? { unit: 'bytes', count: parsed.hex.length / 2 } : null;
27
32
  };
28
33
 
@@ -40,26 +45,35 @@ export const renderView = (view: Exclude<ViewId, 'text'>, draft: Draft): Rendere
40
45
  if (draft.mode !== 'value') {
41
46
  return { kind: 'empty' };
42
47
  }
48
+
43
49
  if (view === 'formatted') {
44
50
  const pretty = prettyJson(draft.text);
51
+
45
52
  if (pretty !== null) {
46
53
  return { kind: 'text', text: pretty };
47
54
  }
55
+
48
56
  const checked = checkJson(draft.text);
57
+
49
58
  return { kind: 'problem', problem: { kind: 'json', reason: checked.ok ? '' : checked.reason } };
50
59
  }
51
60
 
52
61
  const parsed = parseHex(draft.text);
62
+
53
63
  if (!parsed.ok) {
54
64
  return { kind: 'problem', problem: { kind: 'hex', problem: parsed.problem } };
55
65
  }
66
+
56
67
  if (view === 'utf8') {
57
68
  return { kind: 'text', text: decodeUtf8(parsed.hex) };
58
69
  }
70
+
59
71
  if (view === 'uuid') {
60
72
  return { kind: 'text', text: formatUuid(parsed.hex) ?? '' };
61
73
  }
74
+
62
75
  const total = parsed.hex.length / 2;
76
+
63
77
  return {
64
78
  kind: 'text',
65
79
  text: hexDumpLines(parsed.hex, DUMP_LIMIT_BYTES).join('\n'),