@aiot-nexus/plugin-sdk 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +18 -8
  2. package/dev-host/main.js +134 -7
  3. package/dev-host/plugin-host.js +1 -427
  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/internal/src-electron/main/core/runtime-result.js +42 -0
  8. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +890 -0
  9. package/dist/internal/src-electron/plugin-host-bootstrap.js +417 -0
  10. package/dist/internal/src-electron/shared/contracts.js +2 -0
  11. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +181 -0
  12. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +70 -0
  13. package/dist/internal/src-electron/shared/plugin-electron-facade.js +243 -0
  14. package/dist/internal/src-electron/shared/plugin-rpc.js +245 -0
  15. package/dist/src/electron-facade.d.ts +4 -0
  16. package/dist/src/electron-facade.js +56 -0
  17. package/dist/src/index.d.ts +2 -1
  18. package/dist/src/index.js +2 -1
  19. package/dist/src/nexus-dev.js +5 -4
  20. package/dist/src/scaffold.js +2 -2
  21. package/dist/src/types.d.ts +86 -2
  22. package/dist/src/web-dev.js +2 -2
  23. package/package.json +7 -2
  24. package/templates/minimal/README.md.tmpl +35 -2
  25. package/templates/minimal/src/main.ts.tmpl +2 -1
  26. package/templates/sqlite/README.md.tmpl +30 -2
  27. package/templates/sqlite/src/better-sqlite3.d.ts +12 -1
  28. package/templates/sqlite/src/main.ts.tmpl +939 -124
  29. package/templates/sqlite/src/renderer/app.ts.tmpl +688 -181
  30. package/templates/sqlite/src/renderer/index.html +40 -22
  31. package/templates/sqlite/src/renderer/plugin.css +302 -90
@@ -1,3 +1,5 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import Database = require("better-sqlite3");
2
4
  import type {
3
5
  NexusResult,
@@ -6,50 +8,174 @@ import type {
6
8
  PluginRendererOperationRequest
7
9
  } from "@aiot-nexus/plugin-sdk";
8
10
 
9
- interface SqliteRecord {
10
- id: number;
11
- title: string;
12
- note: string;
13
- createdAt: string;
11
+ type DatabaseSource = "default" | "external";
12
+ type SqliteAffinity = "integer" | "real" | "text" | "blob" | "numeric";
13
+ type LocatorKind = "primaryKey" | "rowid";
14
+
15
+ interface DatabaseInfo {
16
+ databaseName: string;
17
+ databasePath: string;
18
+ source: DatabaseSource;
19
+ databaseReady: true;
20
+ notice?: string;
21
+ }
22
+
23
+ interface TableListRow {
24
+ schema: string;
25
+ name: string;
26
+ type: string;
27
+ ncol: number | bigint;
28
+ wr: number | bigint;
29
+ strict: number | bigint;
30
+ }
31
+
32
+ interface TableXinfoRow {
33
+ cid: number | bigint;
34
+ name: string;
35
+ type: string;
36
+ notnull: number | bigint;
37
+ dflt_value: unknown;
38
+ pk: number | bigint;
39
+ hidden: number | bigint;
40
+ }
41
+
42
+ interface TableIndexListRow {
43
+ origin: string;
44
+ }
45
+
46
+ interface TableSummary {
47
+ name: string;
48
+ columnCount: number;
49
+ withoutRowid: boolean;
50
+ strict: boolean;
51
+ }
52
+
53
+ interface ColumnInfo {
54
+ name: string;
55
+ declaredType: string;
56
+ affinity: SqliteAffinity;
57
+ nullable: boolean;
58
+ defaultSql: string | null;
59
+ primaryKeyOrder: number;
60
+ generated: boolean;
61
+ hidden: boolean;
62
+ insertable: boolean;
63
+ requiredOnInsert: boolean;
64
+ autoIntegerPrimaryKey: boolean;
65
+ }
66
+
67
+ interface TableSchema {
68
+ summary: TableSummary;
69
+ columns: ColumnInfo[];
70
+ visibleColumns: ColumnInfo[];
71
+ writableColumns: ColumnInfo[];
72
+ locator?: LocatorSpec;
73
+ }
74
+
75
+ interface LocatorSpec {
76
+ kind: LocatorKind;
77
+ columns: string[];
78
+ rowidAlias?: "rowid" | "_rowid_" | "oid";
79
+ }
80
+
81
+ type WireValue =
82
+ | { kind: "null" }
83
+ | { kind: "integer"; value: string }
84
+ | { kind: "real"; value: number }
85
+ | { kind: "text"; value: string; length: number; truncated: boolean }
86
+ | { kind: "blob"; hex: string; byteLength: number; truncated: boolean };
87
+
88
+ interface LocatorValue {
89
+ column: string;
90
+ value: WireValue;
14
91
  }
15
92
 
16
- const databaseName = "plugin.db";
17
- const tableName = "plugin_records";
18
- let database: Database | undefined;
19
- let databasePath = "";
20
- let databaseCreated = false;
93
+ interface RowLocator {
94
+ kind: LocatorKind;
95
+ values: LocatorValue[];
96
+ }
97
+
98
+ interface TableRow {
99
+ cells: Array<{ column: string; value: WireValue }>;
100
+ locator?: RowLocator;
101
+ }
102
+
103
+ interface ActiveDatabase {
104
+ connection: Database;
105
+ databasePath: string;
106
+ source: DatabaseSource;
107
+ created: boolean;
108
+ }
109
+
110
+ interface PersistedState {
111
+ schemaVersion: 1;
112
+ databasePath: string;
113
+ }
114
+
115
+ interface MutationValue {
116
+ mode: "default" | "unchanged" | "null" | "value";
117
+ value?: string;
118
+ }
119
+
120
+ interface ParsedMutations {
121
+ columns: string[];
122
+ values: unknown[];
123
+ }
124
+
125
+ interface ConvertedInputValue {
126
+ value: unknown;
127
+ byteLength: number;
128
+ }
129
+
130
+ const defaultDatabaseName = "plugin.db";
131
+ const stateFileName = "sqlite-browser-state.json";
132
+ const defaultTableName = "plugin_records";
133
+ const allowedPageSizes = new Set([25, 50, 100]);
134
+ const maximumTextPreviewLength = 16 * 1024;
135
+ const maximumBlobPreviewBytes = 32;
136
+ const maximumFieldBytes = 1024 * 1024;
137
+ const maximumRequestBytes = 4 * 1024 * 1024;
138
+ const minimumSqliteInteger = -(1n << 63n);
139
+ const maximumSqliteInteger = (1n << 63n) - 1n;
140
+
141
+ let activeDatabase: ActiveDatabase | undefined;
142
+ let activationContext: PluginMainActivationContext | undefined;
143
+ let defaultDatabasePath = "";
144
+ let stateFilePath = "";
145
+ let databaseNotice: string | undefined;
146
+ let databaseSelectionPending = false;
21
147
 
22
148
  export async function activate(context: PluginMainActivationContext): Promise<void> {
23
- const fs = require("fs") as typeof import("fs");
24
- const path = require("path") as typeof import("path");
149
+ activationContext = context;
25
150
  fs.mkdirSync(context.host.pluginDataDirectory, { recursive: true });
26
- databasePath = path.join(context.host.pluginDataDirectory, databaseName);
27
- databaseCreated = !fs.existsSync(databasePath);
28
- database = new Database(databasePath);
29
- database.pragma("journal_mode = DELETE");
30
- database.prepare(
31
- "CREATE TABLE IF NOT EXISTS " + tableName + " ("+
32
- "id INTEGER PRIMARY KEY AUTOINCREMENT, "+
33
- "title TEXT NOT NULL, "+
34
- "note TEXT NOT NULL DEFAULT '', "+
35
- "created_at TEXT NOT NULL"+
36
- ")"
37
- ).run();
38
-
39
- await register(context, "sqlite.getInfo", getInfo);
40
- await register(context, "sqlite.listRecords", listRecords);
41
- await register(context, "sqlite.insertRecord", insertRecord);
42
- await register(context, "sqlite.updateRecord", updateRecord);
43
- await register(context, "sqlite.deleteRecord", deleteRecord);
44
- const logResult = await context.host.log("info", "__PLUGIN_ID__ database " + (databaseCreated ? "created" : "loaded") + ": " + databasePath);
151
+ defaultDatabasePath = path.join(context.host.pluginDataDirectory, defaultDatabaseName);
152
+ stateFilePath = path.join(context.host.pluginDataDirectory, stateFileName);
153
+ activeDatabase = openInitialDatabase();
154
+
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(
166
+ "info",
167
+ "__PLUGIN_ID__ database " + (activeDatabase.created ? "created" : "loaded") + " in " + applicationName + ": " + activeDatabase.databasePath
168
+ );
45
169
  if (!logResult.ok) {
46
170
  throw new Error(logResult.error.message);
47
171
  }
48
172
  }
49
173
 
50
174
  export function deactivate(): void {
51
- database?.close();
52
- database = undefined;
175
+ activeDatabase?.connection.close();
176
+ activeDatabase = undefined;
177
+ activationContext = undefined;
178
+ databaseSelectionPending = false;
53
179
  }
54
180
 
55
181
  async function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): Promise<void> {
@@ -59,140 +185,829 @@ async function register(context: PluginMainActivationContext, name: string, hand
59
185
  }
60
186
  }
61
187
 
62
- function getInfo(): Record<string, unknown> {
63
- const activeDatabase = requireDatabase();
64
- const countRow = requireObject(activeDatabase.prepare("SELECT COUNT(*) AS count FROM " + tableName).get(), "count row");
65
- const latestRow = requireObject(activeDatabase.prepare("SELECT MAX(created_at) AS latest FROM " + tableName).get(), "latest row");
188
+ function handled<TResult>(
189
+ action: (request: PluginRendererOperationRequest) => TResult
190
+ ): PluginRendererOperationHandler {
191
+ return (request) => {
192
+ try {
193
+ return success(action(request));
194
+ } catch (error: unknown) {
195
+ return sqliteFailure(error);
196
+ }
197
+ };
198
+ }
199
+
200
+ function handledAsync<TResult>(
201
+ action: (request: PluginRendererOperationRequest) => Promise<TResult>
202
+ ): PluginRendererOperationHandler {
203
+ return async (request) => {
204
+ try {
205
+ return success(await action(request));
206
+ } catch (error: unknown) {
207
+ return sqliteFailure(error);
208
+ }
209
+ };
210
+ }
211
+
212
+ function openInitialDatabase(): ActiveDatabase {
213
+ const persistedPath = readPersistedDatabasePath();
214
+ if (persistedPath) {
215
+ try {
216
+ return openExternalDatabase(persistedPath);
217
+ } catch (error: unknown) {
218
+ databaseNotice = "The recent database could not be reopened. The plugin database is active instead: " + errorMessage(error);
219
+ clearPersistedState();
220
+ }
221
+ }
222
+ return openDefaultDatabase();
223
+ }
224
+
225
+ function openDefaultDatabase(): ActiveDatabase {
226
+ const created = !fs.existsSync(defaultDatabasePath);
227
+ const connection = new Database(defaultDatabasePath);
228
+ try {
229
+ connection.defaultSafeIntegers(true);
230
+ connection.pragma("journal_mode = DELETE");
231
+ connection.prepare(
232
+ "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(defaultTableName) + " ("+
233
+ "id INTEGER PRIMARY KEY AUTOINCREMENT, "+
234
+ "title TEXT NOT NULL, "+
235
+ "note TEXT NOT NULL DEFAULT '', "+
236
+ "created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"+
237
+ ")"
238
+ ).run();
239
+ validateDatabase(connection);
240
+ return { connection, databasePath: defaultDatabasePath, source: "default", created };
241
+ } catch (error: unknown) {
242
+ connection.close();
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ function openExternalDatabase(filePath: string): ActiveDatabase {
248
+ const normalizedPath = path.resolve(filePath);
249
+ const stat = fs.statSync(normalizedPath);
250
+ if (!stat.isFile()) {
251
+ throw new Error("The selected path is not a file.");
252
+ }
253
+ const connection = new Database(normalizedPath, { fileMustExist: true, readonly: false, timeout: 5000 });
254
+ try {
255
+ connection.defaultSafeIntegers(true);
256
+ validateDatabase(connection);
257
+ return { connection, databasePath: normalizedPath, source: "external", created: false };
258
+ } catch (error: unknown) {
259
+ connection.close();
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ function validateDatabase(connection: Database): void {
265
+ connection.prepare("SELECT name FROM main.sqlite_schema ORDER BY name LIMIT 1").get();
266
+ }
267
+
268
+ function getDatabaseInfo(): DatabaseInfo {
269
+ const current = requireDatabase();
270
+ const notice = databaseNotice;
271
+ databaseNotice = undefined;
66
272
  return {
67
- databaseName,
68
- databasePath,
69
- databaseCreated,
70
- tableName,
71
- recordCount: typeof countRow.count === "number" ? countRow.count : 0,
273
+ databaseName: path.basename(current.databasePath),
274
+ databasePath: current.databasePath,
275
+ source: current.source,
72
276
  databaseReady: true,
73
- latestCreatedAt: typeof latestRow.latest === "string" ? latestRow.latest : null
277
+ ...(notice ? { notice } : {})
278
+ };
279
+ }
280
+
281
+ async function chooseDatabase(): Promise<{ canceled: boolean; database: DatabaseInfo }> {
282
+ if (databaseSelectionPending) {
283
+ throw codedError("Busy", "A database selection dialog is already open.");
284
+ }
285
+ const context = requireActivationContext();
286
+ databaseSelectionPending = true;
287
+ try {
288
+ const current = requireDatabase();
289
+ const result = await context.host.electron.dialog.showOpenDialog({
290
+ title: "Open SQLite database",
291
+ defaultPath: path.dirname(current.databasePath),
292
+ properties: ["openFile"],
293
+ filters: [
294
+ { name: "SQLite databases", extensions: ["db", "db3", "sqlite", "sqlite3"] },
295
+ { name: "All files", extensions: ["*"] }
296
+ ]
297
+ });
298
+ if (result.canceled || result.filePaths.length === 0) {
299
+ return { canceled: true, database: getDatabaseInfo() };
300
+ }
301
+ const candidate = openExternalDatabase(result.filePaths[0]);
302
+ try {
303
+ persistDatabasePath(candidate.databasePath);
304
+ replaceDatabase(candidate);
305
+ databaseNotice = undefined;
306
+ } catch (error: unknown) {
307
+ candidate.connection.close();
308
+ throw error;
309
+ }
310
+ await logDatabaseSwitch("external", path.basename(candidate.databasePath));
311
+ return { canceled: false, database: getDatabaseInfo() };
312
+ } finally {
313
+ databaseSelectionPending = false;
314
+ }
315
+ }
316
+
317
+ async function useDefaultDatabase(): Promise<{ database: DatabaseInfo }> {
318
+ const candidate = openDefaultDatabase();
319
+ try {
320
+ clearPersistedState();
321
+ replaceDatabase(candidate);
322
+ databaseNotice = undefined;
323
+ } catch (error: unknown) {
324
+ candidate.connection.close();
325
+ throw error;
326
+ }
327
+ await logDatabaseSwitch("default", defaultDatabaseName);
328
+ return { database: getDatabaseInfo() };
329
+ }
330
+
331
+ async function logDatabaseSwitch(source: DatabaseSource, databaseName: string): Promise<void> {
332
+ const context = activationContext;
333
+ if (!context) {
334
+ return;
335
+ }
336
+ await context.host.log("info", "__PLUGIN_ID__ switched to " + source + " database: " + databaseName);
337
+ }
338
+
339
+ function replaceDatabase(candidate: ActiveDatabase): void {
340
+ const previous = activeDatabase;
341
+ activeDatabase = candidate;
342
+ previous?.connection.close();
343
+ }
344
+
345
+ function listTables(): { tables: TableSummary[] } {
346
+ return { tables: readTableSummaries(requireDatabase().connection) };
347
+ }
348
+
349
+ function readTable(request: PluginRendererOperationRequest): {
350
+ table: TableSummary;
351
+ columns: ColumnInfo[];
352
+ rows: TableRow[];
353
+ totalRows: string;
354
+ offset: number;
355
+ limit: number;
356
+ hasPrevious: boolean;
357
+ hasNext: boolean;
358
+ writable: boolean;
359
+ locatorKind?: LocatorKind;
360
+ } {
361
+ const input = requireInput(request.input);
362
+ const tableName = readRequiredString(input.table, "table", 512);
363
+ const offset = readOffset(input.offset);
364
+ const limit = readPageSize(input.limit);
365
+ const connection = requireDatabase().connection;
366
+ const schema = readTableSchema(connection, tableName);
367
+ 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
+ const selectList = [
371
+ ...(hasRowidLocator ? [schema.locator?.rowidAlias as string] : []),
372
+ ...selectColumns
373
+ ].join(", ");
374
+ const orderBy = orderByClause(schema.locator);
375
+ const rawRows = requireArrayRows(
376
+ connection.prepare(
377
+ "SELECT " + selectList + " FROM " + quoteIdentifier(schema.summary.name) + orderBy + " LIMIT ? OFFSET ?"
378
+ ).raw(true).all(limit, offset)
379
+ );
380
+ const rows = rawRows.map((rawRow) => readTableRow(schema, rawRow));
381
+ return {
382
+ table: schema.summary,
383
+ columns: schema.columns,
384
+ rows,
385
+ totalRows: totalRows.toString(),
386
+ offset,
387
+ limit,
388
+ hasPrevious: offset > 0,
389
+ hasNext: BigInt(offset + rows.length) < totalRows,
390
+ writable: schema.writableColumns.length > 0,
391
+ ...(schema.locator ? { locatorKind: schema.locator.kind } : {})
392
+ };
393
+ }
394
+
395
+ function insertRow(request: PluginRendererOperationRequest): { changes: number; lastInsertRowid: string | null } {
396
+ const input = requireInput(request.input);
397
+ const tableName = readRequiredString(input.table, "table", 512);
398
+ const schema = readTableSchema(requireDatabase().connection, tableName);
399
+ const parsed = parseMutations(schema, input.values, "insert");
400
+ const sql = parsed.columns.length === 0
401
+ ? "INSERT INTO " + quoteIdentifier(schema.summary.name) + " DEFAULT VALUES"
402
+ : "INSERT INTO " + quoteIdentifier(schema.summary.name)
403
+ + " (" + parsed.columns.map(quoteIdentifier).join(", ") + ")"
404
+ + " VALUES (" + parsed.columns.map(() => "?").join(", ") + ")";
405
+ const result = requireDatabase().connection.prepare(sql).run(...parsed.values);
406
+ return {
407
+ changes: result.changes,
408
+ lastInsertRowid: result.lastInsertRowid === undefined || result.lastInsertRowid === null
409
+ ? null
410
+ : String(result.lastInsertRowid)
74
411
  };
75
412
  }
76
413
 
77
- function listRecords(): { records: SqliteRecord[] } {
78
- const rows = requireDatabase().prepare(
79
- "SELECT id, title, note, created_at AS createdAt FROM " + tableName + " ORDER BY id DESC LIMIT 100"
80
- ).all();
81
- return { records: rows.map(readRecordRow) };
414
+ function updateRow(request: PluginRendererOperationRequest): { changes: number } {
415
+ const input = requireInput(request.input);
416
+ const tableName = readRequiredString(input.table, "table", 512);
417
+ const connection = requireDatabase().connection;
418
+ const schema = readTableSchema(connection, tableName);
419
+ const locator = parseLocator(schema, input.locator);
420
+ const parsed = parseMutations(schema, input.values, "update");
421
+ if (parsed.columns.length === 0) {
422
+ throw codedError("InvalidRequest", "Update requires at least one changed column.");
423
+ }
424
+ const where = buildLocatorWhere(schema, locator);
425
+ const sql =
426
+ "UPDATE " + quoteIdentifier(schema.summary.name)
427
+ + " SET " + parsed.columns.map((column) => quoteIdentifier(column) + " = ?").join(", ")
428
+ + " WHERE " + where.sql;
429
+ return { changes: runSingleRowMutation(connection, sql, [...parsed.values, ...where.values]) };
430
+ }
431
+
432
+ function deleteRow(request: PluginRendererOperationRequest): { changes: number } {
433
+ const input = requireInput(request.input);
434
+ const tableName = readRequiredString(input.table, "table", 512);
435
+ const connection = requireDatabase().connection;
436
+ const schema = readTableSchema(connection, tableName);
437
+ const locator = parseLocator(schema, input.locator);
438
+ const where = buildLocatorWhere(schema, locator);
439
+ const sql = "DELETE FROM " + quoteIdentifier(schema.summary.name) + " WHERE " + where.sql;
440
+ return { changes: runSingleRowMutation(connection, sql, where.values) };
441
+ }
442
+
443
+ function runSingleRowMutation(connection: Database, sql: string, values: unknown[]): number {
444
+ return connection.transaction(() => {
445
+ const changes = connection.prepare(sql).run(...values).changes;
446
+ if (changes === 0) {
447
+ throw codedError("NotFound", "The selected row no longer exists.");
448
+ }
449
+ if (changes !== 1) {
450
+ throw codedError("InvalidRequest", "The row locator matched more than one record.");
451
+ }
452
+ return changes;
453
+ })();
454
+ }
455
+
456
+ function readTableSummaries(connection: Database): TableSummary[] {
457
+ return connection.prepare("PRAGMA main.table_list").all().flatMap((value) => {
458
+ const row = requireRecord(value, "table_list row") as unknown as TableListRow;
459
+ if (typeof row.schema !== "string" || typeof row.name !== "string" || typeof row.type !== "string") {
460
+ throw new Error("SQLite table_list row is malformed.");
461
+ }
462
+ if (row.schema !== "main" || row.type !== "table" || row.name.startsWith("sqlite_")) {
463
+ return [];
464
+ }
465
+ return [{
466
+ name: row.name,
467
+ columnCount: integerNumber(row.ncol, "table column count"),
468
+ withoutRowid: integerNumber(row.wr, "without-rowid flag") === 1,
469
+ strict: integerNumber(row.strict, "strict flag") === 1
470
+ }];
471
+ }).sort((left, right) => left.name.localeCompare(right.name, "en-US", { sensitivity: "base" }));
472
+ }
473
+
474
+ function readTableSchema(connection: Database, tableName: string): TableSchema {
475
+ const summary = readTableSummaries(connection).find((table) => table.name === tableName);
476
+ if (!summary) {
477
+ throw codedError("InvalidRequest", "The requested table is not an editable user table.");
478
+ }
479
+ const rows = connection.prepare("PRAGMA main.table_xinfo(" + quoteIdentifier(tableName) + ")").all();
480
+ const rawColumns = rows.map((value) => requireRecord(value, "table_xinfo row") as unknown as TableXinfoRow);
481
+ const primaryKeyCount = rawColumns.filter((column) => integerNumber(column.pk, "primary-key order") > 0).length;
482
+ const hasPrimaryKeyIndex = connection.prepare("PRAGMA main.index_list(" + quoteIdentifier(tableName) + ")").all().some((value) => {
483
+ const row = requireRecord(value, "index_list row") as unknown as TableIndexListRow;
484
+ return row.origin === "pk";
485
+ });
486
+ const columns = rawColumns.map((row): ColumnInfo => {
487
+ if (typeof row.name !== "string" || row.name.length === 0) {
488
+ throw new Error("SQLite table_xinfo row is malformed.");
489
+ }
490
+ const primaryKeyOrder = integerNumber(row.pk, "primary-key order");
491
+ const hiddenValue = integerNumber(row.hidden, "hidden flag");
492
+ const declaredType = typeof row.type === "string" ? row.type : "";
493
+ const defaultSql = row.dflt_value === null || row.dflt_value === undefined ? null : String(row.dflt_value);
494
+ const autoIntegerPrimaryKey = !summary.withoutRowid
495
+ && primaryKeyCount === 1
496
+ && primaryKeyOrder === 1
497
+ && declaredType.trim().toUpperCase() === "INTEGER"
498
+ && !hasPrimaryKeyIndex;
499
+ const generated = hiddenValue === 2 || hiddenValue === 3;
500
+ const hidden = hiddenValue === 1;
501
+ const insertable = !generated && !hidden;
502
+ const implicitNotNullPrimaryKey = primaryKeyOrder > 0
503
+ && (summary.withoutRowid || summary.strict || autoIntegerPrimaryKey);
504
+ const nullable = integerNumber(row.notnull, "not-null flag") === 0 && !implicitNotNullPrimaryKey;
505
+ return {
506
+ name: row.name,
507
+ declaredType,
508
+ affinity: sqliteAffinity(declaredType),
509
+ nullable,
510
+ defaultSql,
511
+ primaryKeyOrder,
512
+ generated,
513
+ hidden,
514
+ insertable,
515
+ requiredOnInsert: insertable && !nullable && defaultSql === null && !autoIntegerPrimaryKey,
516
+ autoIntegerPrimaryKey
517
+ };
518
+ });
519
+ const visibleColumns = columns.filter((column) => !column.hidden);
520
+ const writableColumns = columns.filter((column) => column.insertable);
521
+ return {
522
+ summary,
523
+ columns,
524
+ visibleColumns,
525
+ writableColumns,
526
+ locator: createLocatorSpec(summary, columns)
527
+ };
528
+ }
529
+
530
+ function createLocatorSpec(summary: TableSummary, columns: ColumnInfo[]): LocatorSpec | undefined {
531
+ if (!summary.withoutRowid) {
532
+ const names = new Set(columns.map((column) => column.name.toLowerCase()));
533
+ const rowidAlias = (["rowid", "_rowid_", "oid"] as const).find((name) => !names.has(name));
534
+ if (rowidAlias) {
535
+ return { kind: "rowid", columns: [rowidAlias], rowidAlias };
536
+ }
537
+ }
538
+ const primaryKeyColumns = columns
539
+ .filter((column) => column.primaryKeyOrder > 0)
540
+ .sort((left, right) => left.primaryKeyOrder - right.primaryKeyOrder);
541
+ if (primaryKeyColumns.length > 0 && primaryKeyColumns.every((column) => !column.nullable)) {
542
+ return { kind: "primaryKey", columns: primaryKeyColumns.map((column) => column.name) };
543
+ }
544
+ return undefined;
545
+ }
546
+
547
+ function readTableRow(schema: TableSchema, rawRow: unknown[]): TableRow {
548
+ const rowidOffset = schema.locator?.kind === "rowid" ? 1 : 0;
549
+ if (rawRow.length !== schema.visibleColumns.length + rowidOffset) {
550
+ throw new Error("SQLite table row has an unexpected column count.");
551
+ }
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);
557
+ return { cells, ...(locator ? { locator } : {}) };
558
+ }
559
+
560
+ function createRowLocator(schema: TableSchema, rawRow: unknown[], rowidOffset: number): RowLocator | undefined {
561
+ const spec = schema.locator;
562
+ if (!spec) {
563
+ return undefined;
564
+ }
565
+ if (spec.kind === "rowid") {
566
+ const value = serializeLocatorValue(rawRow[0]);
567
+ return value ? { kind: "rowid", values: [{ column: spec.columns[0], value }] } : undefined;
568
+ }
569
+ 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]);
573
+ if (!value) {
574
+ return undefined;
575
+ }
576
+ values.push({ column: columnName, value });
577
+ }
578
+ return { kind: "primaryKey", values };
82
579
  }
83
580
 
84
- function insertRecord(request: PluginRendererOperationRequest): NexusResult<never> | { record: SqliteRecord } {
85
- const inputResult = readRecordInput(request.input, "Insert");
86
- if (!inputResult.ok) {
87
- return inputResult;
581
+ function serializePreviewValue(value: unknown): WireValue {
582
+ if (value === null || value === undefined) {
583
+ return { kind: "null" };
88
584
  }
89
- const createdAt = new Date().toISOString();
90
- const activeDatabase = requireDatabase();
91
- const insertResult = activeDatabase.prepare(
92
- "INSERT INTO " + tableName + " (title, note, created_at) VALUES (?, ?, ?)"
93
- ).run(inputResult.value.title, inputResult.value.note, createdAt);
94
- const id = Number(insertResult.lastInsertRowid);
95
- const record = readRecord(id);
96
- if (!record) {
97
- throw new Error("Inserted SQLite record could not be read.");
585
+ if (typeof value === "bigint") {
586
+ return { kind: "integer", value: value.toString() };
98
587
  }
99
- return { record };
588
+ if (typeof value === "number") {
589
+ return Number.isInteger(value)
590
+ ? { kind: "integer", value: String(value) }
591
+ : { kind: "real", value };
592
+ }
593
+ if (typeof value === "string") {
594
+ return {
595
+ kind: "text",
596
+ value: value.slice(0, maximumTextPreviewLength),
597
+ length: value.length,
598
+ truncated: value.length > maximumTextPreviewLength
599
+ };
600
+ }
601
+ if (Buffer.isBuffer(value)) {
602
+ return {
603
+ kind: "blob",
604
+ hex: value.subarray(0, maximumBlobPreviewBytes).toString("hex"),
605
+ byteLength: value.length,
606
+ truncated: value.length > maximumBlobPreviewBytes
607
+ };
608
+ }
609
+ throw new Error("SQLite returned an unsupported cell value.");
100
610
  }
101
611
 
102
- function updateRecord(request: PluginRendererOperationRequest): NexusResult<never> | { record: SqliteRecord } {
103
- const idResult = readRecordId(request.input, "Update");
104
- if (!idResult.ok) {
105
- return idResult;
612
+ function serializeLocatorValue(value: unknown): WireValue | undefined {
613
+ if (typeof value === "string" && Buffer.byteLength(value, "utf8") > maximumFieldBytes) {
614
+ return undefined;
106
615
  }
107
- const inputResult = readRecordInput(request.input, "Update");
108
- if (!inputResult.ok) {
109
- return inputResult;
616
+ if (Buffer.isBuffer(value) && value.length > maximumFieldBytes) {
617
+ return undefined;
110
618
  }
111
- const updateResult = requireDatabase().prepare(
112
- "UPDATE " + tableName + " SET title = ?, note = ? WHERE id = ?"
113
- ).run(inputResult.value.title, inputResult.value.note, idResult.value);
114
- if (updateResult.changes === 0) {
115
- return fail("NotFound", "Record was not found.");
619
+ if (typeof value === "string") {
620
+ return { kind: "text", value, length: value.length, truncated: false };
116
621
  }
117
- const record = readRecord(idResult.value);
118
- if (!record) {
119
- throw new Error("Updated SQLite record could not be read.");
622
+ if (Buffer.isBuffer(value)) {
623
+ return { kind: "blob", hex: value.toString("hex"), byteLength: value.length, truncated: false };
120
624
  }
121
- return { record };
625
+ return serializePreviewValue(value);
122
626
  }
123
627
 
124
- function deleteRecord(request: PluginRendererOperationRequest): NexusResult<never> | { deletedId: number; record: SqliteRecord } {
125
- const idResult = readRecordId(request.input, "Delete");
126
- if (!idResult.ok) {
127
- return idResult;
628
+ function parseMutations(schema: TableSchema, value: unknown, mode: "insert" | "update"): ParsedMutations {
629
+ const valuesRecord = requireRecord(value, "column values");
630
+ const knownColumns = new Map(schema.writableColumns.map((column) => [column.name, column]));
631
+ const columns: string[] = [];
632
+ const values: unknown[] = [];
633
+ let requestBytes = 0;
634
+ for (const key of Object.keys(valuesRecord)) {
635
+ const column = knownColumns.get(key);
636
+ if (!column) {
637
+ throw codedError("InvalidRequest", "Column is not writable or does not exist: " + key);
638
+ }
639
+ const mutation = readMutationValue(valuesRecord[key], key);
640
+ if (mutation.mode === "default" || mutation.mode === "unchanged") {
641
+ if (mode === "update" && mutation.mode === "default") {
642
+ throw codedError("InvalidRequest", "Default mode is not supported during update: " + key);
643
+ }
644
+ if (mode === "insert" && mutation.mode === "unchanged") {
645
+ throw codedError("InvalidRequest", "Unchanged mode is not supported during insert: " + key);
646
+ }
647
+ continue;
648
+ }
649
+ if (mutation.mode === "null") {
650
+ if (!column.nullable) {
651
+ throw codedError("InvalidRequest", "Column does not accept NULL: " + key);
652
+ }
653
+ columns.push(key);
654
+ values.push(null);
655
+ continue;
656
+ }
657
+ const converted = convertInputValue(column, mutation.value || "");
658
+ if (converted.byteLength > maximumFieldBytes) {
659
+ throw codedError("InvalidRequest", "Column value exceeds the 1 MiB limit: " + key);
660
+ }
661
+ requestBytes += converted.byteLength;
662
+ if (requestBytes > maximumRequestBytes) {
663
+ throw codedError("InvalidRequest", "Row values exceed the 4 MiB request limit.");
664
+ }
665
+ columns.push(key);
666
+ values.push(converted.value);
128
667
  }
129
- const record = readRecord(idResult.value);
130
- if (!record) {
131
- return fail("NotFound", "Record was not found.");
668
+ if (mode === "insert") {
669
+ for (const column of schema.writableColumns) {
670
+ if (column.requiredOnInsert && !columns.includes(column.name)) {
671
+ throw codedError("InvalidRequest", "A value is required for column: " + column.name);
672
+ }
673
+ }
132
674
  }
133
- requireDatabase().prepare("DELETE FROM " + tableName + " WHERE id = ?").run(idResult.value);
134
- return { deletedId: idResult.value, record };
675
+ return { columns, values };
135
676
  }
136
677
 
137
- function readRecord(id: number): SqliteRecord | undefined {
138
- const row = requireDatabase().prepare(
139
- "SELECT id, title, note, created_at AS createdAt FROM " + tableName + " WHERE id = ?"
140
- ).get(id);
141
- return row === undefined ? undefined : readRecordRow(row);
678
+ function readMutationValue(value: unknown, columnName: string): MutationValue {
679
+ const record = requireRecord(value, "mutation for " + columnName);
680
+ if (record.mode !== "default" && record.mode !== "unchanged" && record.mode !== "null" && record.mode !== "value") {
681
+ throw codedError("InvalidRequest", "Column mutation mode is invalid: " + columnName);
682
+ }
683
+ if (record.mode === "value" && typeof record.value !== "string") {
684
+ throw codedError("InvalidRequest", "Column value must be text: " + columnName);
685
+ }
686
+ return { mode: record.mode, ...(typeof record.value === "string" ? { value: record.value } : {}) };
142
687
  }
143
688
 
144
- function readRecordRow(value: unknown): SqliteRecord {
145
- const row = requireObject(value, "record row");
146
- if (!Number.isSafeInteger(row.id) || typeof row.title !== "string" || typeof row.note !== "string" || typeof row.createdAt !== "string") {
147
- throw new Error("SQLite record row is malformed.");
689
+ 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);
694
+ }
695
+ return { value: parseSqliteInteger(source, column.name), byteLength: sourceBytes };
696
+ }
697
+ if (column.affinity === "real") {
698
+ const parsed = Number(source);
699
+ if (!source.trim() || !Number.isFinite(parsed)) {
700
+ throw codedError("InvalidRequest", "Column requires a finite number: " + column.name);
701
+ }
702
+ return { value: parsed, byteLength: sourceBytes };
148
703
  }
149
- return { id: row.id as number, title: row.title, note: row.note, createdAt: row.createdAt };
704
+ if (column.affinity === "numeric") {
705
+ if (/^[+-]?\d+$/.test(source)) {
706
+ return { value: parseSqliteInteger(source, column.name), byteLength: sourceBytes };
707
+ }
708
+ const parsed = Number(source);
709
+ return {
710
+ value: source.trim() && Number.isFinite(parsed) ? parsed : source,
711
+ byteLength: sourceBytes
712
+ };
713
+ }
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
+ return { value: source, byteLength: sourceBytes };
150
726
  }
151
727
 
152
- function readRecordInput(input: Record<string, unknown> | undefined, action: string): NexusResult<{ title: string; note: string }> {
153
- if (!input) {
154
- return fail("InvalidRequest", action + " input must be an object.");
728
+ function parseSqliteInteger(source: string, columnName: string): bigint {
729
+ const value = BigInt(source);
730
+ if (value < minimumSqliteInteger || value > maximumSqliteInteger) {
731
+ throw codedError("InvalidRequest", "Column integer is outside SQLite's signed 64-bit range: " + columnName);
155
732
  }
156
- const title = typeof input.title === "string" ? input.title.trim() : "";
157
- const note = typeof input.note === "string" ? input.note.trim() : "";
158
- if (title.length < 1 || title.length > 80) {
159
- return fail("InvalidRequest", "Title must be 1 to 80 characters.");
733
+ return value;
734
+ }
735
+
736
+ function parseLocator(schema: TableSchema, value: unknown): RowLocator {
737
+ const spec = schema.locator;
738
+ if (!spec) {
739
+ throw codedError("UnsupportedTable", "This table does not expose a stable row locator.");
160
740
  }
161
- if (note.length > 500) {
162
- return fail("InvalidRequest", "Note must be 500 characters or fewer.");
741
+ const record = requireRecord(value, "row locator");
742
+ if (record.kind !== spec.kind || !Array.isArray(record.values) || record.values.length !== spec.columns.length) {
743
+ throw codedError("InvalidRequest", "Row locator does not match the current table.");
163
744
  }
164
- return { ok: true, value: { title, note } };
745
+ const values = record.values.map((entry, index): LocatorValue => {
746
+ const item = requireRecord(entry, "row locator value");
747
+ const expectedColumn = spec.columns[index];
748
+ if (item.column !== expectedColumn) {
749
+ throw codedError("InvalidRequest", "Row locator columns do not match the current table.");
750
+ }
751
+ return { column: expectedColumn, value: readWireValue(item.value) };
752
+ });
753
+ return { kind: spec.kind, values };
165
754
  }
166
755
 
167
- function readRecordId(input: Record<string, unknown> | undefined, action: string): NexusResult<number> {
168
- if (!input) {
169
- return fail("InvalidRequest", action + " input must be an object.");
756
+ function readWireValue(value: unknown): WireValue {
757
+ const record = requireRecord(value, "wire value");
758
+ if (record.kind === "null") {
759
+ return { kind: "null" };
760
+ }
761
+ if (record.kind === "integer" && typeof record.value === "string" && /^[+-]?\d+$/.test(record.value)) {
762
+ return { kind: "integer", value: record.value };
763
+ }
764
+ if (record.kind === "real" && typeof record.value === "number" && Number.isFinite(record.value)) {
765
+ return { kind: "real", value: record.value };
170
766
  }
171
- const id = typeof input.id === "number"
172
- ? input.id
173
- : typeof input.id === "string"
174
- ? Number(input.id.trim())
175
- : Number.NaN;
176
- if (!Number.isSafeInteger(id) || id < 1) {
177
- return fail("InvalidRequest", action + " record id must be a positive integer.");
767
+ if (record.kind === "text" && typeof record.value === "string" && record.truncated === false
768
+ && Buffer.byteLength(record.value, "utf8") <= maximumFieldBytes) {
769
+ return { kind: "text", value: record.value, length: record.value.length, truncated: false };
178
770
  }
179
- return { ok: true, value: id };
771
+ 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) {
774
+ return { kind: "blob", hex: record.hex, byteLength: record.hex.length / 2, truncated: false };
775
+ }
776
+ throw codedError("InvalidRequest", "Row locator contains an invalid value.");
777
+ }
778
+
779
+ function buildLocatorWhere(schema: TableSchema, locator: RowLocator): { sql: string; values: unknown[] } {
780
+ const spec = schema.locator;
781
+ if (!spec || locator.kind !== spec.kind) {
782
+ throw codedError("InvalidRequest", "Row locator does not match the current table.");
783
+ }
784
+ const expressions = locator.values.map((entry) => {
785
+ return spec.kind === "rowid"
786
+ ? (spec.rowidAlias as string) + " IS ?"
787
+ : quoteIdentifier(entry.column) + " IS ?";
788
+ });
789
+ return { sql: expressions.join(" AND "), values: locator.values.map((entry) => decodeWireValue(entry.value)) };
180
790
  }
181
791
 
182
- function requireDatabase(): Database {
183
- if (!database) {
184
- throw new Error("SQLite database is not initialized.");
792
+ function decodeWireValue(value: WireValue): unknown {
793
+ if (value.kind === "null") {
794
+ return null;
795
+ }
796
+ if (value.kind === "integer") {
797
+ return BigInt(value.value);
798
+ }
799
+ if (value.kind === "real") {
800
+ return value.value;
185
801
  }
186
- return database;
802
+ if (value.kind === "text") {
803
+ return value.value;
804
+ }
805
+ return Buffer.from(value.hex, "hex");
806
+ }
807
+
808
+ function readCount(connection: Database, tableName: string): bigint {
809
+ const row = requireRecord(
810
+ connection.prepare("SELECT COUNT(*) AS count FROM " + quoteIdentifier(tableName)).get(),
811
+ "count row"
812
+ );
813
+ if (typeof row.count === "bigint") {
814
+ return row.count;
815
+ }
816
+ if (typeof row.count === "number" && Number.isSafeInteger(row.count) && row.count >= 0) {
817
+ return BigInt(row.count);
818
+ }
819
+ throw new Error("SQLite count row is malformed.");
820
+ }
821
+
822
+ function orderByClause(locator: LocatorSpec | undefined): string {
823
+ if (!locator) {
824
+ return "";
825
+ }
826
+ if (locator.kind === "rowid") {
827
+ return " ORDER BY " + locator.rowidAlias + " DESC";
828
+ }
829
+ return " ORDER BY " + locator.columns.map((column) => quoteIdentifier(column) + " ASC").join(", ");
830
+ }
831
+
832
+ function sqliteAffinity(declaredType: string): SqliteAffinity {
833
+ const normalized = declaredType.toUpperCase();
834
+ if (normalized.includes("INT")) {
835
+ return "integer";
836
+ }
837
+ if (normalized.includes("CHAR") || normalized.includes("CLOB") || normalized.includes("TEXT")) {
838
+ return "text";
839
+ }
840
+ if (!normalized || normalized.includes("BLOB")) {
841
+ return "blob";
842
+ }
843
+ if (normalized.includes("REAL") || normalized.includes("FLOA") || normalized.includes("DOUB")) {
844
+ return "real";
845
+ }
846
+ return "numeric";
847
+ }
848
+
849
+ function readPersistedDatabasePath(): string | undefined {
850
+ if (!fs.existsSync(stateFilePath)) {
851
+ return undefined;
852
+ }
853
+ try {
854
+ const parsed = JSON.parse(fs.readFileSync(stateFilePath, "utf8")) as Partial<PersistedState>;
855
+ if (parsed.schemaVersion !== 1 || typeof parsed.databasePath !== "string" || !path.isAbsolute(parsed.databasePath)) {
856
+ throw new Error("SQLite browser state is invalid.");
857
+ }
858
+ return path.resolve(parsed.databasePath);
859
+ } catch (error: unknown) {
860
+ const invalidPath = stateFilePath + ".invalid-" + String(Date.now());
861
+ try {
862
+ fs.renameSync(stateFilePath, invalidPath);
863
+ } catch {
864
+ try {
865
+ fs.rmSync(stateFilePath, { force: true });
866
+ } catch {
867
+ // A read-only state file must not prevent the safe default database fallback.
868
+ }
869
+ }
870
+ databaseNotice = "The saved database selection was invalid and has been reset: " + errorMessage(error);
871
+ return undefined;
872
+ }
873
+ }
874
+
875
+ function persistDatabasePath(filePath: string): void {
876
+ writeJsonAtomic(stateFilePath, { schemaVersion: 1, databasePath: path.resolve(filePath) } satisfies PersistedState);
877
+ }
878
+
879
+ function clearPersistedState(): void {
880
+ fs.rmSync(stateFilePath, { force: true });
187
881
  }
188
882
 
189
- function requireObject(value: unknown, label: string): Record<string, unknown> {
883
+ function writeJsonAtomic(filePath: string, value: unknown): void {
884
+ const tempPath = filePath + "." + String(process.pid) + "." + String(Date.now()) + ".tmp";
885
+ fs.writeFileSync(tempPath, JSON.stringify(value, null, 2) + "\n", "utf8");
886
+ try {
887
+ fs.renameSync(tempPath, filePath);
888
+ } finally {
889
+ fs.rmSync(tempPath, { force: true });
890
+ }
891
+ }
892
+
893
+ function readPageSize(value: unknown): number {
894
+ const result = value === undefined ? 50 : Number(value);
895
+ if (!Number.isSafeInteger(result) || !allowedPageSizes.has(result)) {
896
+ throw codedError("InvalidRequest", "Page size must be 25, 50, or 100.");
897
+ }
898
+ return result;
899
+ }
900
+
901
+ function readOffset(value: unknown): number {
902
+ const result = value === undefined ? 0 : Number(value);
903
+ if (!Number.isSafeInteger(result) || result < 0 || result > 1_000_000_000) {
904
+ throw codedError("InvalidRequest", "Offset must be a non-negative integer no greater than 1000000000.");
905
+ }
906
+ return result;
907
+ }
908
+
909
+ function requireInput(value: Record<string, unknown> | undefined): Record<string, unknown> {
910
+ if (!value) {
911
+ throw codedError("InvalidRequest", "Operation input must be an object.");
912
+ }
913
+ return value;
914
+ }
915
+
916
+ function readRequiredString(value: unknown, label: string, maximumLength: number): string {
917
+ if (typeof value !== "string" || value.length < 1 || value.length > maximumLength) {
918
+ throw codedError("InvalidRequest", label + " must be a non-empty string.");
919
+ }
920
+ return value;
921
+ }
922
+
923
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
190
924
  if (!value || typeof value !== "object" || Array.isArray(value)) {
191
- throw new Error("SQLite " + label + " is malformed.");
925
+ throw codedError("InvalidRequest", "SQLite " + label + " must be an object.");
192
926
  }
193
927
  return value as Record<string, unknown>;
194
928
  }
195
929
 
930
+ function requireArrayRows(value: unknown[]): unknown[][] {
931
+ return value.map((row) => {
932
+ if (!Array.isArray(row)) {
933
+ throw new Error("SQLite raw row is malformed.");
934
+ }
935
+ return row;
936
+ });
937
+ }
938
+
939
+ function integerNumber(value: unknown, label: string): number {
940
+ const result = typeof value === "bigint" ? Number(value) : value;
941
+ if (typeof result !== "number" || !Number.isSafeInteger(result)) {
942
+ throw new Error("SQLite " + label + " is malformed.");
943
+ }
944
+ return result;
945
+ }
946
+
947
+ function quoteIdentifier(value: string): string {
948
+ return "\"" + value.replace(/\"/g, "\"\"") + "\"";
949
+ }
950
+
951
+ function requireDatabase(): ActiveDatabase {
952
+ if (!activeDatabase) {
953
+ throw codedError("DatabaseUnavailable", "SQLite database is not initialized.");
954
+ }
955
+ return activeDatabase;
956
+ }
957
+
958
+ function requireActivationContext(): PluginMainActivationContext {
959
+ if (!activationContext) {
960
+ throw codedError("DatabaseUnavailable", "Plugin activation context is unavailable.");
961
+ }
962
+ return activationContext;
963
+ }
964
+
965
+ function success<T>(value: T): NexusResult<T> {
966
+ return { ok: true, value };
967
+ }
968
+
969
+ function sqliteFailure(error: unknown): NexusResult<never> {
970
+ const code = errorCode(error);
971
+ if (code === "InvalidRequest" || code === "NotFound" || code === "UnsupportedTable"
972
+ || code === "Busy" || code === "DatabaseUnavailable") {
973
+ return fail(code, errorMessage(error));
974
+ }
975
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
976
+ return fail("ConstraintViolation", errorMessage(error));
977
+ }
978
+ if (code === "SQLITE_READONLY" || code.startsWith("SQLITE_READONLY_")) {
979
+ return fail("SqliteReadOnly", errorMessage(error));
980
+ }
981
+ if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || code.startsWith("SQLITE_BUSY_") || code.startsWith("SQLITE_LOCKED_")) {
982
+ return fail("SqliteBusy", errorMessage(error));
983
+ }
984
+ if (code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB" || code.startsWith("SQLITE_CORRUPT_")) {
985
+ return fail("SqliteInvalidDatabase", errorMessage(error));
986
+ }
987
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM"
988
+ || code === "SQLITE_CANTOPEN" || code.startsWith("SQLITE_CANTOPEN_")
989
+ || code === "SQLITE_PERM" || code === "SQLITE_AUTH") {
990
+ return fail("SqliteOpenFailed", errorMessage(error));
991
+ }
992
+ return fail("SqliteOperationFailed", errorMessage(error));
993
+ }
994
+
995
+ function codedError(code: string, message: string): Error {
996
+ const error = new Error(message) as Error & { code: string };
997
+ error.code = code;
998
+ return error;
999
+ }
1000
+
1001
+ function errorCode(error: unknown): string {
1002
+ return error && typeof error === "object" && typeof Reflect.get(error, "code") === "string"
1003
+ ? String(Reflect.get(error, "code"))
1004
+ : "";
1005
+ }
1006
+
1007
+ function errorMessage(error: unknown): string {
1008
+ return error instanceof Error ? error.message : String(error);
1009
+ }
1010
+
196
1011
  function fail<T = never>(code: string, message: string): NexusResult<T> {
197
1012
  return { ok: false, error: { code, message } };
198
1013
  }