@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.
- package/README.md +151 -96
- package/dev-host/host.html +13 -12
- package/dev-host/main.js +362 -506
- package/dist/.generated/plugin-framework-version.d.ts +1 -1
- package/dist/.generated/plugin-framework-version.js +1 -1
- package/dist/.generated/plugin-manifest.schema.json +1 -1
- package/dist/browser/frame-client.d.ts +1 -0
- package/dist/browser/frame-client.mjs +121 -13
- package/dist/src/browser/frame-client.d.ts +1 -0
- package/dist/src/browser/frame-client.js +121 -13
- package/dist/src/cli/run.js +2 -2
- package/dist/src/dev.js +3 -3
- package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/nexus-dev-control.d.ts +1 -1
- package/dist/src/nexus-dev-control.js +2 -2
- package/dist/src/nexus-dev.d.ts +0 -3
- package/dist/src/nexus-dev.js +118 -53
- package/dist/src/types.d.ts +14 -98
- package/package.json +1 -1
- package/templates/minimal/.vscode/launch.json +1 -1
- package/templates/minimal/README.md.tmpl +68 -19
- package/templates/minimal/src/main.ts.tmpl +5 -4
- package/templates/sqlite/.vscode/launch.json +1 -1
- package/templates/sqlite/README.md.tmpl +13 -11
- package/templates/sqlite/src/main.ts.tmpl +180 -81
- package/templates/sqlite/src/renderer/app.ts.tmpl +9 -5
- package/dev-host/plugin-host.js +0 -4
- package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
- package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
- package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
- package/dist/internal/src-electron/shared/contracts.js +0 -2
- package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
- package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
- package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
- package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
- /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
|
@@ -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
|
|
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 `
|
|
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
|
|
24
|
+
## Native Electron database dialog
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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 `
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const applicationName =
|
|
165
|
-
const logResult =
|
|
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
|
-
|
|
182
|
-
const result =
|
|
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
|
-
|
|
290
|
+
requireActivationContext();
|
|
286
291
|
databaseSelectionPending = true;
|
|
287
292
|
try {
|
|
288
293
|
const current = requireDatabase();
|
|
289
|
-
const result = await
|
|
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
|
-
|
|
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
|
-
|
|
332
|
+
logDatabaseSwitch("default", defaultDatabaseName);
|
|
328
333
|
return { database: getDatabaseInfo() };
|
|
329
334
|
}
|
|
330
335
|
|
|
331
|
-
|
|
336
|
+
function logDatabaseSwitch(source: DatabaseSource, databaseName: string): void {
|
|
332
337
|
const context = activationContext;
|
|
333
338
|
if (!context) {
|
|
334
339
|
return;
|
|
335
340
|
}
|
|
336
|
-
|
|
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
|
-
...(
|
|
372
|
-
...
|
|
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
|
|
549
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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,
|
|
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(
|
|
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 (
|
|
571
|
-
const
|
|
572
|
-
const value =
|
|
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
|
|
582
|
-
|
|
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
|
|
586
|
-
return { kind: "integer", value:
|
|
635
|
+
if (storageType === "integer" && typeof preview === "bigint") {
|
|
636
|
+
return { kind: "integer", value: preview.toString() };
|
|
587
637
|
}
|
|
588
|
-
if (typeof
|
|
589
|
-
return
|
|
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
|
|
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:
|
|
597
|
-
length:
|
|
598
|
-
truncated:
|
|
650
|
+
value: boundedPreview,
|
|
651
|
+
length: totalCharacters,
|
|
652
|
+
truncated: Buffer.byteLength(boundedPreview, "utf8") < totalBytes
|
|
599
653
|
};
|
|
600
654
|
}
|
|
601
|
-
if (Buffer.isBuffer(
|
|
655
|
+
if (storageType === "blob" && Buffer.isBuffer(preview)) {
|
|
656
|
+
const totalBytes = integerNumber(byteLength, "BLOB byte length");
|
|
602
657
|
return {
|
|
603
658
|
kind: "blob",
|
|
604
|
-
hex:
|
|
605
|
-
byteLength:
|
|
606
|
-
truncated:
|
|
659
|
+
hex: preview.toString("hex"),
|
|
660
|
+
byteLength: totalBytes,
|
|
661
|
+
truncated: preview.length < totalBytes
|
|
607
662
|
};
|
|
608
663
|
}
|
|
609
|
-
throw new Error("SQLite returned
|
|
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 (
|
|
685
|
+
if (value === null || value === undefined) {
|
|
614
686
|
return undefined;
|
|
615
687
|
}
|
|
616
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
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
|
|
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", "
|
|
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"
|
|
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
|
-
&&
|
|
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
|
|
773
|
-
&& record.hex.length
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/dev-host/plugin-host.js
DELETED
|
@@ -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;
|