@aiot-nexus/plugin-sdk 2.1.0 → 3.0.2

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 (38) hide show
  1. package/README.md +151 -96
  2. package/dev-host/host.html +13 -12
  3. package/dev-host/main.js +362 -506
  4. package/dist/.generated/plugin-framework-version.d.ts +1 -1
  5. package/dist/.generated/plugin-framework-version.js +1 -1
  6. package/dist/.generated/plugin-manifest.schema.json +1 -1
  7. package/dist/browser/frame-client.d.ts +1 -0
  8. package/dist/browser/frame-client.mjs +121 -13
  9. package/dist/src/browser/frame-client.d.ts +1 -0
  10. package/dist/src/browser/frame-client.js +121 -13
  11. package/dist/src/cli/run.js +2 -2
  12. package/dist/src/dev.js +3 -3
  13. package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
  14. package/dist/src/index.d.ts +2 -2
  15. package/dist/src/index.js +2 -2
  16. package/dist/src/nexus-dev-control.d.ts +1 -1
  17. package/dist/src/nexus-dev-control.js +2 -2
  18. package/dist/src/nexus-dev.d.ts +0 -3
  19. package/dist/src/nexus-dev.js +118 -53
  20. package/dist/src/types.d.ts +14 -98
  21. package/package.json +1 -1
  22. package/templates/minimal/.vscode/launch.json +1 -1
  23. package/templates/minimal/README.md.tmpl +68 -19
  24. package/templates/minimal/src/main.ts.tmpl +5 -4
  25. package/templates/sqlite/.vscode/launch.json +1 -1
  26. package/templates/sqlite/README.md.tmpl +13 -11
  27. package/templates/sqlite/src/main.ts.tmpl +180 -81
  28. package/templates/sqlite/src/renderer/app.ts.tmpl +9 -5
  29. package/dev-host/plugin-host.js +0 -4
  30. package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
  31. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
  32. package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
  33. package/dist/internal/src-electron/shared/contracts.js +0 -2
  34. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
  35. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
  36. package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
  37. package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
  38. /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
@@ -9,7 +9,7 @@
9
9
  "cwd": "${workspaceFolder}"
10
10
  },
11
11
  {
12
- "name": "Attach Nexus plugin worker (9229)",
12
+ "name": "Attach Nexus plugin main process (9229)",
13
13
  "type": "node",
14
14
  "request": "attach",
15
15
  "address": "127.0.0.1",
@@ -17,16 +17,18 @@ The check follows the npm-configured registry, falls back to the official npm re
17
17
 
18
18
  Every `npm run build` rebuilds `better-sqlite3@8.7.0` for Electron 22.3.27 with `@electron/rebuild@3.7.2` before packaging. `npm run rebuild:native` remains available as a standalone diagnostic command.
19
19
 
20
- The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the host coordinates worker shutdown, atomic replacement, and worker reactivation without changing the Nexus PID. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
20
+ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the CLI coordinates Nexus shutdown, atomic replacement, and restart for trusted `mainEntry` changes. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
21
21
 
22
- The template is a schema-aware SQLite database browser. It opens `plugin.db` in the plugin data directory by default and initializes the `plugin_records` sample table there. **Open Database** calls `context.host.electron.dialog.showOpenDialog()` from `mainEntry`; it accepts a single existing SQLite file, including files without an extension. A regular plugin can use this dialog Facade without `electronHostControl`. The renderer only displays the selected path and invokes parameterized operations. It never receives an Electron object and does not expose a path input, arbitrary SQL, DDL, `ATTACH`, or a SQL console.
22
+ The template is a schema-aware SQLite database browser. It opens `plugin.db` in the plugin data directory by default and initializes the `plugin_records` sample table there. **Open Database** calls Electron's directly imported `dialog.showOpenDialog()` from trusted `mainEntry`; it accepts a single existing SQLite file, including files without an extension. The renderer only displays the selected path and invokes parameterized operations. It never receives an Electron object and does not expose a path input, arbitrary SQL, DDL, `ATTACH`, or a SQL console.
23
23
 
24
- ## Electron Facade database dialog
24
+ ## Native Electron database dialog
25
25
 
26
- The template's `sqlite.chooseDatabase` operation uses the same asynchronous Electron Facade available to other v2 plugins. The essential call in `src/main.ts` is:
26
+ Framework API 3.0 loads trusted `mainEntry` code in the Electron main process. The `sqlite.chooseDatabase` operation imports Electron 22 directly; the essential call in `src/main.ts` is:
27
27
 
28
28
  ```ts
29
- const result = await context.host.electron.dialog.showOpenDialog({
29
+ import { dialog } from "electron";
30
+
31
+ const result = await dialog.showOpenDialog({
30
32
  title: "Open SQLite database",
31
33
  properties: ["openFile"],
32
34
  filters: [
@@ -41,24 +43,24 @@ if (!result.canceled && result.filePaths[0]) {
41
43
  }
42
44
  ```
43
45
 
44
- For related workflows, use `properties: ["openDirectory", "createDirectory"]` to choose a folder or `context.host.electron.dialog.showSaveDialog()` to choose a save path. These calls belong in `mainEntry`; the renderer should invoke a registered business operation such as `sqlite.chooseDatabase` through `client.invokeMain()` and receive only the result it needs. No `electronHostControl` or legacy `dialog` permission is required. Native dialogs are unavailable in `npm run dev:web`; use `npm run dev` or a formal Nexus host.
46
+ For related workflows, use `properties: ["openDirectory", "createDirectory"]` to choose a folder or `dialog.showSaveDialog()` to choose a save path. These calls belong in trusted `mainEntry`; the renderer should invoke a registered business operation such as `sqlite.chooseDatabase` through `client.invokeMain()` and receive only the result it needs. Native dialogs are unavailable in `npm run dev:web`; use `npm run dev` or a formal Nexus host.
45
47
 
46
48
  External files are opened read/write only after a separate candidate connection can query `sqlite_schema`. Canceling the dialog or selecting a corrupt, locked, or inaccessible file keeps the current connection. The latest external path is atomically stored as `sqlite-browser-state.json` in the plugin data directory. Startup restores it when valid; otherwise the template clears the saved selection, falls back to `plugin.db`, and returns a visible notice. External databases do not receive the sample schema or journal pragma.
47
49
 
48
- The table selector lists ordinary user tables from the `main` schema. Dynamic headers show declared type, SQLite affinity, primary-key order, NULL, default, and generated-column metadata. The editor supports default/NULL/value modes for inserts and unchanged/NULL/value modes for updates. INTEGER input uses exact decimal integers, REAL must be finite, NUMERIC accepts integer, real, or text values, and BLOB input uses even-length hexadecimal text. The 1 MiB field and 4 MiB row limits count decoded BLOB bytes rather than hexadecimal characters. Ordinary rowid tables prefer an unshadowed built-in rowid; `WITHOUT ROWID` tables, or tables that shadow every rowid alias, fall back only to primary-key columns that are all non-NULL. A table without a unique locator remains browsable and insertable, but update and delete are unavailable. Update and delete run in a transaction and must affect exactly one row. TEXT previews are capped at 16 KiB; BLOB previews show the byte length and first 32 bytes.
50
+ The table selector lists ordinary user tables from the `main` schema. Dynamic headers show declared type, SQLite affinity, primary-key order, NULL, default, and generated-column metadata. The editor supports default/NULL/value modes for inserts and unchanged/NULL/value modes for updates. INTEGER input uses exact decimal integers, REAL must be finite, NUMERIC accepts integer, real, or text values, and BLOB input uses even-length hexadecimal text. The 1 MiB field and 4 MiB row limits count decoded BLOB bytes rather than hexadecimal characters. Ordinary rowid tables prefer an unshadowed built-in rowid; `WITHOUT ROWID` tables, or tables that shadow every rowid alias, fall back only to primary-key columns that are all non-NULL. A table without a unique locator remains browsable and insertable, but update and delete are unavailable. Primary-key locators larger than 16 KiB are also treated as browse-only so a page cannot copy unbounded keys into the trusted main process and iframe. Update and delete run in a transaction and must affect exactly one row. TEXT previews are capped at 16 KiB of UTF-8 data; BLOB previews show the byte length and first 32 bytes.
49
51
 
50
- The renderer follows `context.language` and `context.themeMode` from Nexus and preserves unfinished editor values during in-place context updates. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host. This SDK template deliberately has no Quasar dependency, so its data grid uses a native `<table>` as a dependency-minimized template exception. It still provides pagination, contained horizontal scrolling, light/dark themes, `en-US` / `zh-CN`, and busy/empty/error states. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
52
+ The renderer follows `context.language` and `context.themeMode` from Nexus and preserves unfinished editor values during in-place context updates. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. This SDK template deliberately has no Quasar dependency, so its data grid uses a native `<table>` as a dependency-minimized template exception. It still provides pagination, contained horizontal scrolling, light/dark themes, `en-US` / `zh-CN`, and busy/empty/error states. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
51
53
 
52
54
  The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
53
55
 
54
- Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, create a worker, provide Electron Facade, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. The page explicitly reports `PluginCapabilityUnsupported`; file selection, table loading, and CRUD require `npm run dev` or a formal Nexus host. Pass `-- --no-open` or `-- --port <port>` when needed.
56
+ Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, provide Node or Electron main-process APIs, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. The page explicitly reports `PluginCapabilityUnsupported`; file selection, table loading, and CRUD require `npm run dev` or a formal Nexus host. Pass `-- --no-open` or `-- --port <port>` when needed.
55
57
 
56
- Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop the old plugin worker, replace the package, and start a new worker in the same Nexus process. The database remains under the host-provided plugin data directory across worker replacements. Failed builds keep the previous deployment, and a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
58
+ Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or restart-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop Nexus, replace the package, and start a new Nexus process. The database remains under the host-provided plugin data directory across host restarts. Failed builds keep the previous deployment, and launch or readiness failures roll the package back and restore the previous host. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
57
59
 
58
60
  When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed.
59
61
 
60
62
  For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; fully offline work can disable checks with the environment variables above.
61
63
 
62
- For plugin-worker breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. If child-process auto attach is disabled, start `npm run dev:debug` and select `Attach Nexus plugin worker (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
64
+ For trusted-main breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. To attach manually, start `npm run dev:debug` and select `Attach Nexus plugin main process (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
63
65
 
64
66
  The independent host stores the SQLite database and settings under `.nexus-plugin-dev/data/` and mirrors the PluginFrame protocol and current Host API surface. Its simulated Nexus application version defaults to the version recorded by the SDK build; pass `npm run dev -- --host-version <X.Y.Z>` to exercise another compatibility branch. Run the deployable `dist/__PLUGIN_ID__/` in Nexus before delivery because the standalone host does not reproduce the full application shell or routing state.
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import Database = require("better-sqlite3");
4
+ import { app, dialog } from "electron";
4
5
  import type {
5
6
  NexusResult,
6
7
  PluginMainActivationContext,
@@ -131,10 +132,14 @@ const defaultDatabaseName = "plugin.db";
131
132
  const stateFileName = "sqlite-browser-state.json";
132
133
  const defaultTableName = "plugin_records";
133
134
  const allowedPageSizes = new Set([25, 50, 100]);
134
- const maximumTextPreviewLength = 16 * 1024;
135
+ const maximumTextPreviewBytes = 16 * 1024;
135
136
  const maximumBlobPreviewBytes = 32;
136
137
  const maximumFieldBytes = 1024 * 1024;
137
138
  const maximumRequestBytes = 4 * 1024 * 1024;
139
+ const maximumBlobInputCharacters = maximumFieldBytes * 2 + 4096;
140
+ const maximumLocatorBytes = 16 * 1024;
141
+ const maximumSqliteIntegerCharacters = 20;
142
+ const projectedValuesPerColumn = 4;
138
143
  const minimumSqliteInteger = -(1n << 63n);
139
144
  const maximumSqliteInteger = (1n << 63n) - 1n;
140
145
 
@@ -145,24 +150,24 @@ let stateFilePath = "";
145
150
  let databaseNotice: string | undefined;
146
151
  let databaseSelectionPending = false;
147
152
 
148
- export async function activate(context: PluginMainActivationContext): Promise<void> {
153
+ export function activate(context: PluginMainActivationContext): void {
149
154
  activationContext = context;
150
155
  fs.mkdirSync(context.host.pluginDataDirectory, { recursive: true });
151
156
  defaultDatabasePath = path.join(context.host.pluginDataDirectory, defaultDatabaseName);
152
157
  stateFilePath = path.join(context.host.pluginDataDirectory, stateFileName);
153
158
  activeDatabase = openInitialDatabase();
154
159
 
155
- await register(context, "sqlite.getDatabaseInfo", handled(getDatabaseInfo));
156
- await register(context, "sqlite.chooseDatabase", handledAsync(chooseDatabase));
157
- await register(context, "sqlite.useDefaultDatabase", handledAsync(useDefaultDatabase));
158
- await register(context, "sqlite.listTables", handled(listTables));
159
- await register(context, "sqlite.readTable", handled(readTable));
160
- await register(context, "sqlite.insertRow", handled(insertRow));
161
- await register(context, "sqlite.updateRow", handled(updateRow));
162
- await register(context, "sqlite.deleteRow", handled(deleteRow));
163
-
164
- const applicationName = await context.host.electron.app.getName();
165
- const logResult = await context.host.log(
160
+ register(context, "sqlite.getDatabaseInfo", handled(getDatabaseInfo));
161
+ register(context, "sqlite.chooseDatabase", handledAsync(chooseDatabase));
162
+ register(context, "sqlite.useDefaultDatabase", handledAsync(useDefaultDatabase));
163
+ register(context, "sqlite.listTables", handled(listTables));
164
+ register(context, "sqlite.readTable", handled(readTable));
165
+ register(context, "sqlite.insertRow", handled(insertRow));
166
+ register(context, "sqlite.updateRow", handled(updateRow));
167
+ register(context, "sqlite.deleteRow", handled(deleteRow));
168
+
169
+ const applicationName = app.getName();
170
+ const logResult = context.host.log(
166
171
  "info",
167
172
  "__PLUGIN_ID__ database " + (activeDatabase.created ? "created" : "loaded") + " in " + applicationName + ": " + activeDatabase.databasePath
168
173
  );
@@ -178,8 +183,8 @@ export function deactivate(): void {
178
183
  databaseSelectionPending = false;
179
184
  }
180
185
 
181
- async function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): Promise<void> {
182
- const result = await context.host.registerRendererMethod(name, handler);
186
+ function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): void {
187
+ const result = context.host.registerRendererMethod(name, handler);
183
188
  if (!result.ok) {
184
189
  throw new Error(result.error.message);
185
190
  }
@@ -282,11 +287,11 @@ async function chooseDatabase(): Promise<{ canceled: boolean; database: Database
282
287
  if (databaseSelectionPending) {
283
288
  throw codedError("Busy", "A database selection dialog is already open.");
284
289
  }
285
- const context = requireActivationContext();
290
+ requireActivationContext();
286
291
  databaseSelectionPending = true;
287
292
  try {
288
293
  const current = requireDatabase();
289
- const result = await context.host.electron.dialog.showOpenDialog({
294
+ const result = await dialog.showOpenDialog({
290
295
  title: "Open SQLite database",
291
296
  defaultPath: path.dirname(current.databasePath),
292
297
  properties: ["openFile"],
@@ -307,7 +312,7 @@ async function chooseDatabase(): Promise<{ canceled: boolean; database: Database
307
312
  candidate.connection.close();
308
313
  throw error;
309
314
  }
310
- await logDatabaseSwitch("external", path.basename(candidate.databasePath));
315
+ logDatabaseSwitch("external", path.basename(candidate.databasePath));
311
316
  return { canceled: false, database: getDatabaseInfo() };
312
317
  } finally {
313
318
  databaseSelectionPending = false;
@@ -324,16 +329,19 @@ async function useDefaultDatabase(): Promise<{ database: DatabaseInfo }> {
324
329
  candidate.connection.close();
325
330
  throw error;
326
331
  }
327
- await logDatabaseSwitch("default", defaultDatabaseName);
332
+ logDatabaseSwitch("default", defaultDatabaseName);
328
333
  return { database: getDatabaseInfo() };
329
334
  }
330
335
 
331
- async function logDatabaseSwitch(source: DatabaseSource, databaseName: string): Promise<void> {
336
+ function logDatabaseSwitch(source: DatabaseSource, databaseName: string): void {
332
337
  const context = activationContext;
333
338
  if (!context) {
334
339
  return;
335
340
  }
336
- await context.host.log("info", "__PLUGIN_ID__ switched to " + source + " database: " + databaseName);
341
+ const result = context.host.log("info", "__PLUGIN_ID__ switched to " + source + " database: " + databaseName);
342
+ if (!result.ok) {
343
+ throw new Error(result.error.message);
344
+ }
337
345
  }
338
346
 
339
347
  function replaceDatabase(candidate: ActiveDatabase): void {
@@ -365,11 +373,9 @@ function readTable(request: PluginRendererOperationRequest): {
365
373
  const connection = requireDatabase().connection;
366
374
  const schema = readTableSchema(connection, tableName);
367
375
  const totalRows = readCount(connection, schema.summary.name);
368
- const selectColumns = schema.visibleColumns.map((column) => quoteIdentifier(column.name));
369
- const hasRowidLocator = schema.locator?.kind === "rowid" && schema.locator.rowidAlias;
370
376
  const selectList = [
371
- ...(hasRowidLocator ? [schema.locator?.rowidAlias as string] : []),
372
- ...selectColumns
377
+ ...locatorProjection(schema),
378
+ ...schema.visibleColumns.flatMap(previewProjection)
373
379
  ].join(", ");
374
380
  const orderBy = orderByClause(schema.locator);
375
381
  const rawRows = requireArrayRows(
@@ -544,32 +550,71 @@ function createLocatorSpec(summary: TableSummary, columns: ColumnInfo[]): Locato
544
550
  return undefined;
545
551
  }
546
552
 
553
+ function locatorProjection(schema: TableSchema): string[] {
554
+ const spec = schema.locator;
555
+ if (!spec) {
556
+ return [];
557
+ }
558
+ if (spec.kind === "rowid") {
559
+ return [spec.rowidAlias as string];
560
+ }
561
+ return spec.columns.map((column) => {
562
+ const identifier = quoteIdentifier(column);
563
+ return "CASE WHEN typeof(" + identifier + ") IN ('text', 'blob')"
564
+ + " AND length(CAST(" + identifier + " AS BLOB)) > " + String(maximumLocatorBytes)
565
+ + " THEN NULL ELSE " + identifier + " END";
566
+ });
567
+ }
568
+
569
+ function previewProjection(column: ColumnInfo): string[] {
570
+ const identifier = quoteIdentifier(column.name);
571
+ const storageType = "typeof(" + identifier + ")";
572
+ return [
573
+ storageType,
574
+ "CASE WHEN " + storageType + " = 'text' THEN length(" + identifier + ") ELSE NULL END",
575
+ "CASE WHEN " + storageType + " IN ('text', 'blob') THEN length(CAST(" + identifier + " AS BLOB)) ELSE NULL END",
576
+ "CASE " + storageType
577
+ + " WHEN 'text' THEN substr(" + identifier + ", 1, " + String(maximumTextPreviewBytes) + ")"
578
+ + " WHEN 'blob' THEN substr(" + identifier + ", 1, " + String(maximumBlobPreviewBytes) + ")"
579
+ + " ELSE " + identifier + " END"
580
+ ];
581
+ }
582
+
547
583
  function readTableRow(schema: TableSchema, rawRow: unknown[]): TableRow {
548
- const rowidOffset = schema.locator?.kind === "rowid" ? 1 : 0;
549
- if (rawRow.length !== schema.visibleColumns.length + rowidOffset) {
584
+ const locatorValueCount = schema.locator?.columns.length || 0;
585
+ const expectedValueCount = locatorValueCount + schema.visibleColumns.length * projectedValuesPerColumn;
586
+ if (rawRow.length !== expectedValueCount) {
550
587
  throw new Error("SQLite table row has an unexpected column count.");
551
588
  }
552
- const cells = schema.visibleColumns.map((column, index) => ({
553
- column: column.name,
554
- value: serializePreviewValue(rawRow[index + rowidOffset])
555
- }));
556
- const locator = createRowLocator(schema, rawRow, rowidOffset);
589
+ const cells = schema.visibleColumns.map((column, index) => {
590
+ const valueOffset = locatorValueCount + index * projectedValuesPerColumn;
591
+ return {
592
+ column: column.name,
593
+ value: serializeProjectedValue(
594
+ rawRow[valueOffset],
595
+ rawRow[valueOffset + 1],
596
+ rawRow[valueOffset + 2],
597
+ rawRow[valueOffset + 3]
598
+ )
599
+ };
600
+ });
601
+ const locator = createRowLocator(schema, rawRow.slice(0, locatorValueCount));
557
602
  return { cells, ...(locator ? { locator } : {}) };
558
603
  }
559
604
 
560
- function createRowLocator(schema: TableSchema, rawRow: unknown[], rowidOffset: number): RowLocator | undefined {
605
+ function createRowLocator(schema: TableSchema, rawLocatorValues: unknown[]): RowLocator | undefined {
561
606
  const spec = schema.locator;
562
607
  if (!spec) {
563
608
  return undefined;
564
609
  }
565
610
  if (spec.kind === "rowid") {
566
- const value = serializeLocatorValue(rawRow[0]);
611
+ const value = serializeLocatorValue(rawLocatorValues[0]);
567
612
  return value ? { kind: "rowid", values: [{ column: spec.columns[0], value }] } : undefined;
568
613
  }
569
614
  const values: LocatorValue[] = [];
570
- for (const columnName of spec.columns) {
571
- const index = schema.visibleColumns.findIndex((column) => column.name === columnName);
572
- const value = index < 0 ? undefined : serializeLocatorValue(rawRow[index + rowidOffset]);
615
+ for (let index = 0; index < spec.columns.length; index += 1) {
616
+ const columnName = spec.columns[index];
617
+ const value = serializeLocatorValue(rawLocatorValues[index]);
573
618
  if (!value) {
574
619
  return undefined;
575
620
  }
@@ -578,42 +623,73 @@ function createRowLocator(schema: TableSchema, rawRow: unknown[], rowidOffset: n
578
623
  return { kind: "primaryKey", values };
579
624
  }
580
625
 
581
- function serializePreviewValue(value: unknown): WireValue {
582
- if (value === null || value === undefined) {
626
+ function serializeProjectedValue(
627
+ storageType: unknown,
628
+ characterLength: unknown,
629
+ byteLength: unknown,
630
+ preview: unknown
631
+ ): WireValue {
632
+ if (storageType === "null" && (preview === null || preview === undefined)) {
583
633
  return { kind: "null" };
584
634
  }
585
- if (typeof value === "bigint") {
586
- return { kind: "integer", value: value.toString() };
635
+ if (storageType === "integer" && typeof preview === "bigint") {
636
+ return { kind: "integer", value: preview.toString() };
587
637
  }
588
- if (typeof value === "number") {
589
- return Number.isInteger(value)
590
- ? { kind: "integer", value: String(value) }
591
- : { kind: "real", value };
638
+ if (storageType === "integer" && typeof preview === "number" && Number.isSafeInteger(preview)) {
639
+ return { kind: "integer", value: String(preview) };
592
640
  }
593
- if (typeof value === "string") {
641
+ if (storageType === "real" && typeof preview === "number" && Number.isFinite(preview)) {
642
+ return { kind: "real", value: preview };
643
+ }
644
+ if (storageType === "text" && typeof preview === "string") {
645
+ const totalCharacters = integerNumber(characterLength, "text character length");
646
+ const totalBytes = integerNumber(byteLength, "text byte length");
647
+ const boundedPreview = truncateUtf8(preview, maximumTextPreviewBytes);
594
648
  return {
595
649
  kind: "text",
596
- value: value.slice(0, maximumTextPreviewLength),
597
- length: value.length,
598
- truncated: value.length > maximumTextPreviewLength
650
+ value: boundedPreview,
651
+ length: totalCharacters,
652
+ truncated: Buffer.byteLength(boundedPreview, "utf8") < totalBytes
599
653
  };
600
654
  }
601
- if (Buffer.isBuffer(value)) {
655
+ if (storageType === "blob" && Buffer.isBuffer(preview)) {
656
+ const totalBytes = integerNumber(byteLength, "BLOB byte length");
602
657
  return {
603
658
  kind: "blob",
604
- hex: value.subarray(0, maximumBlobPreviewBytes).toString("hex"),
605
- byteLength: value.length,
606
- truncated: value.length > maximumBlobPreviewBytes
659
+ hex: preview.toString("hex"),
660
+ byteLength: totalBytes,
661
+ truncated: preview.length < totalBytes
607
662
  };
608
663
  }
609
- throw new Error("SQLite returned an unsupported cell value.");
664
+ throw new Error("SQLite returned malformed projected cell data.");
665
+ }
666
+
667
+ function truncateUtf8(value: string, maximumBytes: number): string {
668
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes) {
669
+ return value;
670
+ }
671
+ const characters: string[] = [];
672
+ let byteLength = 0;
673
+ for (const character of value) {
674
+ const characterBytes = Buffer.byteLength(character, "utf8");
675
+ if (byteLength + characterBytes > maximumBytes) {
676
+ break;
677
+ }
678
+ characters.push(character);
679
+ byteLength += characterBytes;
680
+ }
681
+ return characters.join("");
610
682
  }
611
683
 
612
684
  function serializeLocatorValue(value: unknown): WireValue | undefined {
613
- if (typeof value === "string" && Buffer.byteLength(value, "utf8") > maximumFieldBytes) {
685
+ if (value === null || value === undefined) {
614
686
  return undefined;
615
687
  }
616
- if (Buffer.isBuffer(value) && value.length > maximumFieldBytes) {
688
+ if (typeof value === "string" && (value.length > maximumLocatorBytes
689
+ || Buffer.byteLength(value, "utf8") > maximumLocatorBytes)) {
690
+ return undefined;
691
+ }
692
+ if (Buffer.isBuffer(value) && value.length > maximumLocatorBytes) {
617
693
  return undefined;
618
694
  }
619
695
  if (typeof value === "string") {
@@ -622,7 +698,15 @@ function serializeLocatorValue(value: unknown): WireValue | undefined {
622
698
  if (Buffer.isBuffer(value)) {
623
699
  return { kind: "blob", hex: value.toString("hex"), byteLength: value.length, truncated: false };
624
700
  }
625
- return serializePreviewValue(value);
701
+ if (typeof value === "bigint") {
702
+ return { kind: "integer", value: value.toString() };
703
+ }
704
+ if (typeof value === "number" && Number.isFinite(value)) {
705
+ return Number.isInteger(value)
706
+ ? { kind: "integer", value: String(value) }
707
+ : { kind: "real", value };
708
+ }
709
+ return undefined;
626
710
  }
627
711
 
628
712
  function parseMutations(schema: TableSchema, value: unknown, mode: "insert" | "update"): ParsedMutations {
@@ -687,12 +771,23 @@ function readMutationValue(value: unknown, columnName: string): MutationValue {
687
771
  }
688
772
 
689
773
  function convertInputValue(column: ColumnInfo, source: string): ConvertedInputValue {
690
- const sourceBytes = Buffer.byteLength(source, "utf8");
691
- if (column.affinity === "integer") {
692
- if (!/^[+-]?\d+$/.test(source)) {
693
- throw codedError("InvalidRequest", "Column requires an integer: " + column.name);
774
+ if (column.affinity === "blob") {
775
+ if (source.length > maximumBlobInputCharacters) {
776
+ throw codedError("InvalidRequest", "BLOB value exceeds the accepted encoded input size: " + column.name);
777
+ }
778
+ const normalized = source.replace(/\s+/g, "");
779
+ if (normalized.length > maximumFieldBytes * 2) {
780
+ throw codedError("InvalidRequest", "BLOB value exceeds the 1 MiB limit: " + column.name);
694
781
  }
695
- return { value: parseSqliteInteger(source, column.name), byteLength: sourceBytes };
782
+ if (normalized.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(normalized)) {
783
+ throw codedError("InvalidRequest", "BLOB columns require an even-length hexadecimal value: " + column.name);
784
+ }
785
+ const buffer = Buffer.from(normalized, "hex");
786
+ return { value: buffer, byteLength: buffer.length };
787
+ }
788
+ const sourceBytes = boundedUtf8ByteLength(source, maximumFieldBytes, "Column value exceeds the 1 MiB limit: " + column.name);
789
+ if (column.affinity === "integer") {
790
+ return { value: parseSqliteInteger(source, "Column " + column.name), byteLength: sourceBytes };
696
791
  }
697
792
  if (column.affinity === "real") {
698
793
  const parsed = Number(source);
@@ -703,7 +798,7 @@ function convertInputValue(column: ColumnInfo, source: string): ConvertedInputVa
703
798
  }
704
799
  if (column.affinity === "numeric") {
705
800
  if (/^[+-]?\d+$/.test(source)) {
706
- return { value: parseSqliteInteger(source, column.name), byteLength: sourceBytes };
801
+ return { value: parseSqliteInteger(source, "Column " + column.name), byteLength: sourceBytes };
707
802
  }
708
803
  const parsed = Number(source);
709
804
  return {
@@ -711,24 +806,27 @@ function convertInputValue(column: ColumnInfo, source: string): ConvertedInputVa
711
806
  byteLength: sourceBytes
712
807
  };
713
808
  }
714
- if (column.affinity === "blob") {
715
- const normalized = source.replace(/\s+/g, "");
716
- if (normalized.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(normalized)) {
717
- throw codedError("InvalidRequest", "BLOB columns require an even-length hexadecimal value: " + column.name);
718
- }
719
- if (normalized.length > maximumFieldBytes * 2) {
720
- throw codedError("InvalidRequest", "BLOB value exceeds the 1 MiB limit: " + column.name);
721
- }
722
- const buffer = Buffer.from(normalized, "hex");
723
- return { value: buffer, byteLength: buffer.length };
724
- }
725
809
  return { value: source, byteLength: sourceBytes };
726
810
  }
727
811
 
728
- function parseSqliteInteger(source: string, columnName: string): bigint {
812
+ function boundedUtf8ByteLength(source: string, maximumBytes: number, message: string): number {
813
+ if (source.length > maximumBytes) {
814
+ throw codedError("InvalidRequest", message);
815
+ }
816
+ const byteLength = Buffer.byteLength(source, "utf8");
817
+ if (byteLength > maximumBytes) {
818
+ throw codedError("InvalidRequest", message);
819
+ }
820
+ return byteLength;
821
+ }
822
+
823
+ function parseSqliteInteger(source: string, label: string): bigint {
824
+ if (source.length > maximumSqliteIntegerCharacters || !/^[+-]?\d+$/.test(source)) {
825
+ throw codedError("InvalidRequest", label + " requires a signed 64-bit integer.");
826
+ }
729
827
  const value = BigInt(source);
730
828
  if (value < minimumSqliteInteger || value > maximumSqliteInteger) {
731
- throw codedError("InvalidRequest", "Column integer is outside SQLite's signed 64-bit range: " + columnName);
829
+ throw codedError("InvalidRequest", label + " is outside SQLite's signed 64-bit range.");
732
830
  }
733
831
  return value;
734
832
  }
@@ -758,19 +856,20 @@ function readWireValue(value: unknown): WireValue {
758
856
  if (record.kind === "null") {
759
857
  return { kind: "null" };
760
858
  }
761
- if (record.kind === "integer" && typeof record.value === "string" && /^[+-]?\d+$/.test(record.value)) {
762
- return { kind: "integer", value: record.value };
859
+ if (record.kind === "integer" && typeof record.value === "string") {
860
+ return { kind: "integer", value: parseSqliteInteger(record.value, "Row locator").toString() };
763
861
  }
764
862
  if (record.kind === "real" && typeof record.value === "number" && Number.isFinite(record.value)) {
765
863
  return { kind: "real", value: record.value };
766
864
  }
767
865
  if (record.kind === "text" && typeof record.value === "string" && record.truncated === false
768
- && Buffer.byteLength(record.value, "utf8") <= maximumFieldBytes) {
866
+ && record.value.length <= maximumLocatorBytes
867
+ && Buffer.byteLength(record.value, "utf8") <= maximumLocatorBytes) {
769
868
  return { kind: "text", value: record.value, length: record.value.length, truncated: false };
770
869
  }
771
870
  if (record.kind === "blob" && typeof record.hex === "string" && record.truncated === false
772
- && record.hex.length % 2 === 0 && /^[0-9a-fA-F]*$/.test(record.hex)
773
- && record.hex.length / 2 <= maximumFieldBytes) {
871
+ && record.hex.length <= maximumLocatorBytes * 2
872
+ && record.hex.length % 2 === 0 && /^[0-9a-fA-F]*$/.test(record.hex)) {
774
873
  return { kind: "blob", hex: record.hex, byteLength: record.hex.length / 2, truncated: false };
775
874
  }
776
875
  throw codedError("InvalidRequest", "Row locator contains an invalid value.");
@@ -327,7 +327,7 @@ void client.waitForContext().then((context) => {
327
327
  writeStatusMessage("error.initializationFailed", { message: errorMessage(error) });
328
328
  });
329
329
 
330
- async function refreshDatabase(preserveSelection: boolean): Promise<void> {
330
+ async function refreshDatabase(preserveSelection: boolean): Promise<boolean> {
331
331
  setBusy(true, "status.loading");
332
332
  try {
333
333
  const [info, tableList] = await Promise.all([
@@ -351,10 +351,12 @@ async function refreshDatabase(preserveSelection: boolean): Promise<void> {
351
351
  } else {
352
352
  writeStatusMessage("status.ready");
353
353
  }
354
+ return true;
354
355
  } catch (error: unknown) {
355
356
  tableData = undefined;
356
357
  renderTableData();
357
358
  writeStatusMessage("error.refreshFailed", { message: errorMessage(error) });
359
+ return false;
358
360
  } finally {
359
361
  setBusy(false, "status.idle");
360
362
  }
@@ -371,8 +373,9 @@ async function openDatabase(): Promise<void> {
371
373
  closeEditor();
372
374
  selectedTable = "";
373
375
  offset = 0;
374
- await refreshDatabase(false);
375
- writeStatusMessage("status.databaseOpened", { name: result.database.databaseName });
376
+ if (await refreshDatabase(false)) {
377
+ writeStatusMessage("status.databaseOpened", { name: result.database.databaseName });
378
+ }
376
379
  } catch (error: unknown) {
377
380
  writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
378
381
  } finally {
@@ -387,8 +390,9 @@ async function useDefaultDatabase(): Promise<void> {
387
390
  closeEditor();
388
391
  selectedTable = "plugin_records";
389
392
  offset = 0;
390
- await refreshDatabase(true);
391
- writeStatusMessage("status.defaultOpened");
393
+ if (await refreshDatabase(true)) {
394
+ writeStatusMessage("status.defaultOpened");
395
+ }
392
396
  } catch (error: unknown) {
393
397
  writeStatusMessage("error.operationFailed", { message: errorMessage(error) });
394
398
  } finally {
@@ -1,4 +0,0 @@
1
- "use strict";
2
- /* eslint-disable @typescript-eslint/no-var-requires */
3
-
4
- require("../dist/internal/src-electron/plugin-host-bootstrap.js");
@@ -1,42 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fail = exports.ok = exports.StatusCodes = void 0;
4
- exports.StatusCodes = {
5
- InvalidRequest: "InvalidRequest",
6
- NotFound: "NotFound",
7
- UnexpectedFailure: "UnexpectedFailure",
8
- ExternalAppDefinitionMissing: "ExternalAppDefinitionMissing",
9
- ExternalAppDisabled: "ExternalAppDisabled",
10
- ExternalAppLaunchSettingsMissing: "ExternalAppLaunchSettingsMissing",
11
- ExternalAppExecutableNotConfigured: "ExternalAppExecutableNotConfigured",
12
- ExternalAppExecutableInvalid: "ExternalAppExecutableInvalid",
13
- ExternalAppExecutableMissing: "ExternalAppExecutableMissing",
14
- ExternalAppWorkingDirectoryInvalid: "ExternalAppWorkingDirectoryInvalid",
15
- ExternalAppWorkingDirectoryMissing: "ExternalAppWorkingDirectoryMissing",
16
- ExternalAppStartFailed: "ExternalAppStartFailed",
17
- DirectoryMissing: "DirectoryMissing",
18
- PluginBlocked: "PluginBlocked",
19
- PluginManifestInvalid: "PluginManifestInvalid",
20
- PluginIncompatible: "PluginIncompatible",
21
- PluginPermissionDenied: "PluginPermissionDenied",
22
- PluginCapabilityUnsupported: "PluginCapabilityUnsupported",
23
- PluginMainEntryFailed: "PluginMainEntryFailed",
24
- ToolFileReadFailed: "ToolFileReadFailed",
25
- XxbpDeferred: "XxbpDeferred",
26
- XxbpInvalidRemotePath: "XxbpInvalidRemotePath",
27
- XxbpToolUnavailable: "XxbpToolUnavailable",
28
- XxbpCommandFailed: "XxbpCommandFailed",
29
- XxbpCommandTimeout: "XxbpCommandTimeout",
30
- XxbpOperationCancelled: "XxbpOperationCancelled",
31
- XxbpDevicesSessionMissing: "XxbpDevicesSessionMissing",
32
- XxbpResultFileMissing: "XxbpResultFileMissing",
33
- XxbpNonTextFile: "XxbpNonTextFile"
34
- };
35
- function ok(value) {
36
- return { ok: true, value };
37
- }
38
- exports.ok = ok;
39
- function fail(code, message, details) {
40
- return { ok: false, error: { code, message, details } };
41
- }
42
- exports.fail = fail;