@aigne/afs-sqlite 1.1.0-beta.1 → 1.11.0-beta
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 +51 -36
- package/dist/index.cjs +1324 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +758 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1299 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -44
- package/CHANGELOG.md +0 -61
- package/lib/cjs/actions/built-in.d.ts +0 -5
- package/lib/cjs/actions/built-in.js +0 -165
- package/lib/cjs/actions/registry.d.ts +0 -49
- package/lib/cjs/actions/registry.js +0 -102
- package/lib/cjs/actions/types.d.ts +0 -51
- package/lib/cjs/actions/types.js +0 -2
- package/lib/cjs/config.d.ts +0 -89
- package/lib/cjs/config.js +0 -33
- package/lib/cjs/index.d.ts +0 -13
- package/lib/cjs/index.js +0 -47
- package/lib/cjs/node/builder.d.ts +0 -43
- package/lib/cjs/node/builder.js +0 -187
- package/lib/cjs/operations/crud.d.ts +0 -64
- package/lib/cjs/operations/crud.js +0 -225
- package/lib/cjs/operations/query-builder.d.ts +0 -37
- package/lib/cjs/operations/query-builder.js +0 -102
- package/lib/cjs/operations/search.d.ts +0 -75
- package/lib/cjs/operations/search.js +0 -172
- package/lib/cjs/package.json +0 -3
- package/lib/cjs/router/path-router.d.ts +0 -38
- package/lib/cjs/router/path-router.js +0 -90
- package/lib/cjs/router/types.d.ts +0 -30
- package/lib/cjs/router/types.js +0 -2
- package/lib/cjs/schema/introspector.d.ts +0 -48
- package/lib/cjs/schema/introspector.js +0 -186
- package/lib/cjs/schema/types.d.ts +0 -104
- package/lib/cjs/schema/types.js +0 -13
- package/lib/cjs/sqlite-afs.d.ts +0 -144
- package/lib/cjs/sqlite-afs.js +0 -337
- package/lib/dts/actions/built-in.d.ts +0 -5
- package/lib/dts/actions/registry.d.ts +0 -49
- package/lib/dts/actions/types.d.ts +0 -51
- package/lib/dts/config.d.ts +0 -89
- package/lib/dts/index.d.ts +0 -13
- package/lib/dts/node/builder.d.ts +0 -43
- package/lib/dts/operations/crud.d.ts +0 -64
- package/lib/dts/operations/query-builder.d.ts +0 -37
- package/lib/dts/operations/search.d.ts +0 -75
- package/lib/dts/router/path-router.d.ts +0 -38
- package/lib/dts/router/types.d.ts +0 -30
- package/lib/dts/schema/introspector.d.ts +0 -48
- package/lib/dts/schema/types.d.ts +0 -104
- package/lib/dts/sqlite-afs.d.ts +0 -144
- package/lib/esm/actions/built-in.d.ts +0 -5
- package/lib/esm/actions/built-in.js +0 -162
- package/lib/esm/actions/registry.d.ts +0 -49
- package/lib/esm/actions/registry.js +0 -98
- package/lib/esm/actions/types.d.ts +0 -51
- package/lib/esm/actions/types.js +0 -1
- package/lib/esm/config.d.ts +0 -89
- package/lib/esm/config.js +0 -30
- package/lib/esm/index.d.ts +0 -13
- package/lib/esm/index.js +0 -17
- package/lib/esm/node/builder.d.ts +0 -43
- package/lib/esm/node/builder.js +0 -177
- package/lib/esm/operations/crud.d.ts +0 -64
- package/lib/esm/operations/crud.js +0 -221
- package/lib/esm/operations/query-builder.d.ts +0 -37
- package/lib/esm/operations/query-builder.js +0 -92
- package/lib/esm/operations/search.d.ts +0 -75
- package/lib/esm/operations/search.js +0 -167
- package/lib/esm/package.json +0 -3
- package/lib/esm/router/path-router.d.ts +0 -38
- package/lib/esm/router/path-router.js +0 -83
- package/lib/esm/router/types.d.ts +0 -30
- package/lib/esm/router/types.js +0 -1
- package/lib/esm/schema/introspector.d.ts +0 -48
- package/lib/esm/schema/introspector.js +0 -182
- package/lib/esm/schema/types.d.ts +0 -104
- package/lib/esm/schema/types.js +0 -10
- package/lib/esm/sqlite-afs.d.ts +0 -144
- package/lib/esm/sqlite-afs.js +0 -333
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
import { initDatabase, sql } from "@aigne/sqlite";
|
|
2
|
+
import { accessModeSchema } from "@aigne/afs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createRouter } from "radix3";
|
|
5
|
+
|
|
6
|
+
//#region src/actions/built-in.ts
|
|
7
|
+
/**
|
|
8
|
+
* Executes a raw SQL query and returns all rows
|
|
9
|
+
*/
|
|
10
|
+
async function execAll$3(db, query) {
|
|
11
|
+
return db.all(sql.raw(query)).execute();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Executes a raw SQL query (for INSERT, UPDATE, DELETE)
|
|
15
|
+
*/
|
|
16
|
+
async function execRun$2(db, query) {
|
|
17
|
+
await db.run(sql.raw(query)).execute();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Registers built-in actions to the registry
|
|
21
|
+
*/
|
|
22
|
+
function registerBuiltInActions(registry) {
|
|
23
|
+
registry.register({
|
|
24
|
+
name: "refresh",
|
|
25
|
+
description: "Refresh the schema cache for this module",
|
|
26
|
+
tableLevel: true,
|
|
27
|
+
rowLevel: false,
|
|
28
|
+
handler: async (ctx) => {
|
|
29
|
+
await ctx.module.refreshSchema();
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
message: "Schema refreshed successfully"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
registry.register({
|
|
37
|
+
name: "export",
|
|
38
|
+
description: "Export table data in specified format (json, csv)",
|
|
39
|
+
tableLevel: true,
|
|
40
|
+
rowLevel: false,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: { format: {
|
|
44
|
+
type: "string",
|
|
45
|
+
enum: ["json", "csv"],
|
|
46
|
+
default: "json"
|
|
47
|
+
} }
|
|
48
|
+
},
|
|
49
|
+
handler: async (ctx, params) => {
|
|
50
|
+
const format = params.format ?? "json";
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
data: await ctx.module.exportTable(ctx.table, format)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
registry.register({
|
|
58
|
+
name: "count",
|
|
59
|
+
description: "Get the total row count for this table",
|
|
60
|
+
tableLevel: true,
|
|
61
|
+
rowLevel: false,
|
|
62
|
+
handler: async (ctx) => {
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
data: { count: (await execAll$3(ctx.db, `SELECT COUNT(*) as count FROM "${ctx.table}"`))[0]?.count ?? 0 }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
registry.register({
|
|
70
|
+
name: "duplicate",
|
|
71
|
+
description: "Create a copy of this row",
|
|
72
|
+
tableLevel: false,
|
|
73
|
+
rowLevel: true,
|
|
74
|
+
handler: async (ctx) => {
|
|
75
|
+
if (!ctx.row) return {
|
|
76
|
+
success: false,
|
|
77
|
+
message: "Row data not available"
|
|
78
|
+
};
|
|
79
|
+
const schema = ctx.schemas.get(ctx.table);
|
|
80
|
+
if (!schema) return {
|
|
81
|
+
success: false,
|
|
82
|
+
message: `Table '${ctx.table}' not found`
|
|
83
|
+
};
|
|
84
|
+
const pkColumn = schema.primaryKey[0] ?? "rowid";
|
|
85
|
+
const rowCopy = { ...ctx.row };
|
|
86
|
+
delete rowCopy[pkColumn];
|
|
87
|
+
delete rowCopy.rowid;
|
|
88
|
+
const columns = Object.keys(rowCopy);
|
|
89
|
+
const values = columns.map((col) => formatValueForSQL(rowCopy[col]));
|
|
90
|
+
await execRun$2(ctx.db, `INSERT INTO "${ctx.table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${values.join(", ")})`);
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
data: { newId: (await execAll$3(ctx.db, "SELECT last_insert_rowid() as id"))[0]?.id },
|
|
94
|
+
message: "Row duplicated successfully"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
registry.register({
|
|
99
|
+
name: "validate",
|
|
100
|
+
description: "Validate row data against schema constraints",
|
|
101
|
+
tableLevel: false,
|
|
102
|
+
rowLevel: true,
|
|
103
|
+
handler: async (ctx) => {
|
|
104
|
+
if (!ctx.row) return {
|
|
105
|
+
success: false,
|
|
106
|
+
message: "Row data not available"
|
|
107
|
+
};
|
|
108
|
+
const schema = ctx.schemas.get(ctx.table);
|
|
109
|
+
if (!schema) return {
|
|
110
|
+
success: false,
|
|
111
|
+
message: `Table '${ctx.table}' not found`
|
|
112
|
+
};
|
|
113
|
+
const errors = [];
|
|
114
|
+
for (const col of schema.columns) if (col.notnull && (ctx.row[col.name] === null || ctx.row[col.name] === void 0)) errors.push(`Column '${col.name}' cannot be null`);
|
|
115
|
+
for (const fk of schema.foreignKeys) {
|
|
116
|
+
const value = ctx.row[fk.from];
|
|
117
|
+
if (value !== null && value !== void 0) {
|
|
118
|
+
if ((await execAll$3(ctx.db, `SELECT COUNT(*) as count FROM "${fk.table}" WHERE "${fk.to}" = '${String(value).replace(/'/g, "''")}'`))[0]?.count === 0) errors.push(`Foreign key violation: ${fk.from} references non-existent ${fk.table}.${fk.to}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
success: errors.length === 0,
|
|
123
|
+
data: {
|
|
124
|
+
errors,
|
|
125
|
+
valid: errors.length === 0
|
|
126
|
+
},
|
|
127
|
+
message: errors.length > 0 ? `Validation failed: ${errors.join("; ")}` : "Row is valid"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Formats a value for SQL insertion
|
|
134
|
+
*/
|
|
135
|
+
function formatValueForSQL(value) {
|
|
136
|
+
if (value === null || value === void 0) return "NULL";
|
|
137
|
+
if (typeof value === "number") return String(value);
|
|
138
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
139
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
140
|
+
if (typeof value === "object") return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
141
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/actions/registry.ts
|
|
146
|
+
/**
|
|
147
|
+
* Registry for managing action handlers
|
|
148
|
+
*/
|
|
149
|
+
var ActionsRegistry = class {
|
|
150
|
+
handlers = /* @__PURE__ */ new Map();
|
|
151
|
+
/**
|
|
152
|
+
* Registers an action handler
|
|
153
|
+
*/
|
|
154
|
+
register(definition) {
|
|
155
|
+
this.handlers.set(definition.name, definition);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Registers a simple action with just name and handler
|
|
159
|
+
*/
|
|
160
|
+
registerSimple(name, handler, options) {
|
|
161
|
+
this.register({
|
|
162
|
+
name,
|
|
163
|
+
handler,
|
|
164
|
+
description: options?.description,
|
|
165
|
+
tableLevel: options?.tableLevel ?? false,
|
|
166
|
+
rowLevel: options?.rowLevel ?? true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Unregisters an action
|
|
171
|
+
*/
|
|
172
|
+
unregister(name) {
|
|
173
|
+
return this.handlers.delete(name);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Checks if an action is registered
|
|
177
|
+
*/
|
|
178
|
+
has(name) {
|
|
179
|
+
return this.handlers.has(name);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Gets an action definition
|
|
183
|
+
*/
|
|
184
|
+
get(name) {
|
|
185
|
+
return this.handlers.get(name);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Lists all registered actions
|
|
189
|
+
*/
|
|
190
|
+
list(options) {
|
|
191
|
+
const actions = Array.from(this.handlers.values());
|
|
192
|
+
if (options?.tableLevel !== void 0 || options?.rowLevel !== void 0) return actions.filter((a) => {
|
|
193
|
+
if (options.tableLevel && !a.tableLevel) return false;
|
|
194
|
+
if (options.rowLevel && !a.rowLevel) return false;
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
return actions;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Lists action names
|
|
201
|
+
*/
|
|
202
|
+
listNames(options) {
|
|
203
|
+
return this.list(options).map((a) => a.name);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Executes an action
|
|
207
|
+
*/
|
|
208
|
+
async execute(name, ctx, params = {}) {
|
|
209
|
+
const definition = this.handlers.get(name);
|
|
210
|
+
if (!definition) return {
|
|
211
|
+
success: false,
|
|
212
|
+
message: `Unknown action: ${name}`
|
|
213
|
+
};
|
|
214
|
+
if (ctx.pk && !definition.rowLevel) return {
|
|
215
|
+
success: false,
|
|
216
|
+
message: `Action '${name}' is not available at row level`
|
|
217
|
+
};
|
|
218
|
+
if (!ctx.pk && !definition.tableLevel) return {
|
|
219
|
+
success: false,
|
|
220
|
+
message: `Action '${name}' is not available at table level`
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
return await definition.handler(ctx, params);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
message: error instanceof Error ? error.message : String(error)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/config.ts
|
|
235
|
+
/**
|
|
236
|
+
* FTS (Full-Text Search) configuration schema
|
|
237
|
+
*/
|
|
238
|
+
const ftsConfigSchema = z.object({
|
|
239
|
+
enabled: z.boolean().default(true).describe("Whether FTS is enabled"),
|
|
240
|
+
tables: z.record(z.array(z.string())).optional().describe("Map of table name to columns to index for FTS")
|
|
241
|
+
}).optional();
|
|
242
|
+
/**
|
|
243
|
+
* SQLite AFS module configuration schema
|
|
244
|
+
*/
|
|
245
|
+
const sqliteAFSConfigSchema = z.object({
|
|
246
|
+
url: z.string().describe("SQLite database URL (file:./path or :memory:)"),
|
|
247
|
+
name: z.string().optional().describe("Module name, defaults to 'sqlite-afs'"),
|
|
248
|
+
description: z.string().optional().describe("Description of this module"),
|
|
249
|
+
accessMode: accessModeSchema,
|
|
250
|
+
tables: z.array(z.string()).optional().describe("Whitelist of tables to expose (if not specified, all tables are exposed)"),
|
|
251
|
+
excludeTables: z.array(z.string()).optional().describe("Tables to exclude from exposure"),
|
|
252
|
+
fts: ftsConfigSchema,
|
|
253
|
+
wal: z.boolean().optional().default(true).describe("Enable WAL mode for better concurrency")
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/node/builder.ts
|
|
258
|
+
/**
|
|
259
|
+
* Builds an AFSEntry from a database row
|
|
260
|
+
*/
|
|
261
|
+
function buildRowEntry(table, schema, row, options) {
|
|
262
|
+
const pkColumn = schema.primaryKey[0] ?? "rowid";
|
|
263
|
+
const pk = String(row[pkColumn] ?? row.rowid);
|
|
264
|
+
const basePath = options?.basePath ?? "";
|
|
265
|
+
return {
|
|
266
|
+
id: `${table}:${pk}`,
|
|
267
|
+
path: `${basePath}/${table}/${pk}`,
|
|
268
|
+
content: row,
|
|
269
|
+
metadata: {
|
|
270
|
+
table,
|
|
271
|
+
primaryKey: pkColumn,
|
|
272
|
+
primaryKeyValue: pk
|
|
273
|
+
},
|
|
274
|
+
createdAt: parseDate(row.created_at ?? row.createdAt),
|
|
275
|
+
updatedAt: parseDate(row.updated_at ?? row.updatedAt)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Builds an AFSEntry for a table listing
|
|
280
|
+
*/
|
|
281
|
+
function buildTableEntry(table, schema, options) {
|
|
282
|
+
return {
|
|
283
|
+
id: table,
|
|
284
|
+
path: `${options?.basePath ?? ""}/${table}`,
|
|
285
|
+
description: `Table: ${table} (${schema.columns.length} columns)`,
|
|
286
|
+
metadata: {
|
|
287
|
+
table,
|
|
288
|
+
columnCount: schema.columns.length,
|
|
289
|
+
primaryKey: schema.primaryKey,
|
|
290
|
+
childrenCount: options?.rowCount
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Builds an AFSEntry for table schema
|
|
296
|
+
*/
|
|
297
|
+
function buildSchemaEntry(table, schema, options) {
|
|
298
|
+
const basePath = options?.basePath ?? "";
|
|
299
|
+
return {
|
|
300
|
+
id: `${table}:@schema`,
|
|
301
|
+
path: `${basePath}/${table}/@schema`,
|
|
302
|
+
description: `Schema for table: ${table}`,
|
|
303
|
+
content: {
|
|
304
|
+
name: schema.name,
|
|
305
|
+
columns: schema.columns.map((col) => ({
|
|
306
|
+
name: col.name,
|
|
307
|
+
type: col.type,
|
|
308
|
+
nullable: !col.notnull,
|
|
309
|
+
primaryKey: col.pk > 0,
|
|
310
|
+
defaultValue: col.dfltValue
|
|
311
|
+
})),
|
|
312
|
+
primaryKey: schema.primaryKey,
|
|
313
|
+
foreignKeys: schema.foreignKeys.map((fk) => ({
|
|
314
|
+
column: fk.from,
|
|
315
|
+
references: {
|
|
316
|
+
table: fk.table,
|
|
317
|
+
column: fk.to
|
|
318
|
+
},
|
|
319
|
+
onUpdate: fk.onUpdate,
|
|
320
|
+
onDelete: fk.onDelete
|
|
321
|
+
})),
|
|
322
|
+
indexes: schema.indexes.map((idx) => ({
|
|
323
|
+
name: idx.name,
|
|
324
|
+
unique: idx.unique,
|
|
325
|
+
origin: idx.origin
|
|
326
|
+
}))
|
|
327
|
+
},
|
|
328
|
+
metadata: {
|
|
329
|
+
table,
|
|
330
|
+
type: "schema"
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Builds an AFSEntry for an attribute (single column value)
|
|
336
|
+
*/
|
|
337
|
+
function buildAttributeEntry(table, pk, column, value, options) {
|
|
338
|
+
const basePath = options?.basePath ?? "";
|
|
339
|
+
return {
|
|
340
|
+
id: `${table}:${pk}:@attr:${column}`,
|
|
341
|
+
path: `${basePath}/${table}/${pk}/@attr/${column}`,
|
|
342
|
+
content: value,
|
|
343
|
+
metadata: {
|
|
344
|
+
table,
|
|
345
|
+
primaryKeyValue: pk,
|
|
346
|
+
column,
|
|
347
|
+
type: "attribute"
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Builds an AFSEntry listing all attributes for a row
|
|
353
|
+
*/
|
|
354
|
+
function buildAttributeListEntry(table, schema, pk, row, options) {
|
|
355
|
+
const basePath = options?.basePath ?? "";
|
|
356
|
+
return schema.columns.map((col) => ({
|
|
357
|
+
id: `${table}:${pk}:@attr:${col.name}`,
|
|
358
|
+
path: `${basePath}/${table}/${pk}/@attr/${col.name}`,
|
|
359
|
+
summary: col.name,
|
|
360
|
+
description: `${col.type}${col.notnull ? " NOT NULL" : ""}`,
|
|
361
|
+
content: row[col.name],
|
|
362
|
+
metadata: {
|
|
363
|
+
column: col.name,
|
|
364
|
+
type: col.type
|
|
365
|
+
}
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Builds an AFSEntry for row metadata
|
|
370
|
+
*/
|
|
371
|
+
function buildMetaEntry(table, schema, pk, row, options) {
|
|
372
|
+
const basePath = options?.basePath ?? "";
|
|
373
|
+
return {
|
|
374
|
+
id: `${table}:${pk}:@meta`,
|
|
375
|
+
path: `${basePath}/${table}/${pk}/@meta`,
|
|
376
|
+
content: {
|
|
377
|
+
table,
|
|
378
|
+
primaryKey: schema.primaryKey[0] ?? "rowid",
|
|
379
|
+
primaryKeyValue: pk,
|
|
380
|
+
schema: {
|
|
381
|
+
columns: schema.columns.map((c) => c.name),
|
|
382
|
+
types: Object.fromEntries(schema.columns.map((c) => [c.name, c.type]))
|
|
383
|
+
},
|
|
384
|
+
foreignKeys: schema.foreignKeys.filter((fk) => Object.keys(row).includes(fk.from)),
|
|
385
|
+
rowid: row.rowid
|
|
386
|
+
},
|
|
387
|
+
metadata: {
|
|
388
|
+
table,
|
|
389
|
+
type: "meta"
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Builds AFSEntry for actions list
|
|
395
|
+
*/
|
|
396
|
+
function buildActionsListEntry(table, pk, actions, options) {
|
|
397
|
+
const basePath = options?.basePath ?? "";
|
|
398
|
+
return actions.map((action) => ({
|
|
399
|
+
id: `${table}:${pk}:@actions:${action}`,
|
|
400
|
+
path: `${basePath}/${table}/${pk}/@actions/${action}`,
|
|
401
|
+
summary: action,
|
|
402
|
+
metadata: { execute: {
|
|
403
|
+
name: action,
|
|
404
|
+
description: `Execute ${action} action on ${table}:${pk}`
|
|
405
|
+
} }
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Builds a search result entry with highlights
|
|
410
|
+
*/
|
|
411
|
+
function buildSearchEntry(table, schema, row, snippet, options) {
|
|
412
|
+
const entry = buildRowEntry(table, schema, row, options);
|
|
413
|
+
if (snippet) entry.summary = snippet;
|
|
414
|
+
return entry;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Parses a date from various formats
|
|
418
|
+
*/
|
|
419
|
+
function parseDate(value) {
|
|
420
|
+
if (!value) return void 0;
|
|
421
|
+
if (value instanceof Date) return value;
|
|
422
|
+
if (typeof value === "string") return new Date(value);
|
|
423
|
+
if (typeof value === "number") return new Date(value);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/operations/query-builder.ts
|
|
428
|
+
/**
|
|
429
|
+
* Builds a SELECT query string for a single row by primary key
|
|
430
|
+
*/
|
|
431
|
+
function buildSelectByPK(tableName, schema, pk) {
|
|
432
|
+
return `SELECT * FROM "${tableName}" WHERE "${schema.primaryKey[0] ?? "rowid"}" = '${escapeSQLString(pk)}'`;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Builds a SELECT query string for listing rows with optional limit and offset
|
|
436
|
+
*/
|
|
437
|
+
function buildSelectAll(tableName, options) {
|
|
438
|
+
let query = `SELECT * FROM "${tableName}"`;
|
|
439
|
+
if (options?.orderBy?.length) {
|
|
440
|
+
const orderClauses = options.orderBy.map(([col, dir]) => `"${col}" ${dir.toUpperCase()}`);
|
|
441
|
+
query += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
442
|
+
}
|
|
443
|
+
if (options?.limit !== void 0) query += ` LIMIT ${options.limit}`;
|
|
444
|
+
if (options?.offset !== void 0) query += ` OFFSET ${options.offset}`;
|
|
445
|
+
return query;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Builds an INSERT query string from content object
|
|
449
|
+
*/
|
|
450
|
+
function buildInsert(tableName, schema, content) {
|
|
451
|
+
const validColumns = new Set(schema.columns.map((c) => c.name));
|
|
452
|
+
const entries = Object.entries(content).filter(([key]) => validColumns.has(key));
|
|
453
|
+
if (entries.length === 0) throw new Error(`No valid columns provided for INSERT into ${tableName}`);
|
|
454
|
+
return `INSERT INTO "${tableName}" (${entries.map(([key]) => `"${key}"`).join(", ")}) VALUES (${entries.map(([, value]) => formatValue(value)).join(", ")})`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Builds an UPDATE query string from content object
|
|
458
|
+
*/
|
|
459
|
+
function buildUpdate(tableName, schema, pk, content) {
|
|
460
|
+
const pkColumn = schema.primaryKey[0] ?? "rowid";
|
|
461
|
+
const validColumns = new Set(schema.columns.map((c) => c.name));
|
|
462
|
+
const entries = Object.entries(content).filter(([key]) => validColumns.has(key) && key !== pkColumn);
|
|
463
|
+
if (entries.length === 0) throw new Error(`No valid columns provided for UPDATE on ${tableName}`);
|
|
464
|
+
return `UPDATE "${tableName}" SET ${entries.map(([key, value]) => `"${key}" = ${formatValue(value)}`).join(", ")} WHERE "${pkColumn}" = '${escapeSQLString(pk)}'`;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Builds a DELETE query string by primary key
|
|
468
|
+
*/
|
|
469
|
+
function buildDelete(tableName, schema, pk) {
|
|
470
|
+
return `DELETE FROM "${tableName}" WHERE "${schema.primaryKey[0] ?? "rowid"}" = '${escapeSQLString(pk)}'`;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Formats a value for SQL insertion
|
|
474
|
+
*/
|
|
475
|
+
function formatValue(value) {
|
|
476
|
+
if (value === null || value === void 0) return "NULL";
|
|
477
|
+
if (typeof value === "number") return String(value);
|
|
478
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
479
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
480
|
+
if (typeof value === "object") return `'${escapeSQLString(JSON.stringify(value))}'`;
|
|
481
|
+
return `'${escapeSQLString(String(value))}'`;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Escapes a string for safe SQL insertion
|
|
485
|
+
*/
|
|
486
|
+
function escapeSQLString(str) {
|
|
487
|
+
return str.replace(/'/g, "''");
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Gets the last inserted rowid query string
|
|
491
|
+
*/
|
|
492
|
+
function buildGetLastRowId() {
|
|
493
|
+
return "SELECT last_insert_rowid() as id";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/operations/crud.ts
|
|
498
|
+
/**
|
|
499
|
+
* Executes a raw SQL query and returns all rows
|
|
500
|
+
*/
|
|
501
|
+
async function execAll$2(db, query) {
|
|
502
|
+
return db.all(sql.raw(query)).execute();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Executes a raw SQL query (for INSERT, UPDATE, DELETE)
|
|
506
|
+
*/
|
|
507
|
+
async function execRun$1(db, query) {
|
|
508
|
+
await db.run(sql.raw(query)).execute();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* CRUD operations for SQLite AFS
|
|
512
|
+
*/
|
|
513
|
+
var CRUDOperations = class {
|
|
514
|
+
constructor(db, schemas, basePath = "") {
|
|
515
|
+
this.db = db;
|
|
516
|
+
this.schemas = schemas;
|
|
517
|
+
this.basePath = basePath;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Lists all tables
|
|
521
|
+
*/
|
|
522
|
+
async listTables() {
|
|
523
|
+
const entries = [];
|
|
524
|
+
const buildOptions = { basePath: this.basePath };
|
|
525
|
+
for (const [name, schema] of this.schemas) {
|
|
526
|
+
const rowCount = (await execAll$2(this.db, `SELECT COUNT(*) as count FROM "${name}"`))[0]?.count ?? 0;
|
|
527
|
+
entries.push(buildTableEntry(name, schema, {
|
|
528
|
+
...buildOptions,
|
|
529
|
+
rowCount
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
return { data: entries };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Lists rows in a table
|
|
536
|
+
*/
|
|
537
|
+
async listTable(table, options) {
|
|
538
|
+
const schema = this.schemas.get(table);
|
|
539
|
+
if (!schema) return {
|
|
540
|
+
data: [],
|
|
541
|
+
message: `Table '${table}' not found`
|
|
542
|
+
};
|
|
543
|
+
const buildOptions = { basePath: this.basePath };
|
|
544
|
+
const queryStr = buildSelectAll(table, {
|
|
545
|
+
limit: options?.limit ?? 100,
|
|
546
|
+
orderBy: options?.orderBy
|
|
547
|
+
});
|
|
548
|
+
return { data: (await execAll$2(this.db, queryStr)).map((row) => buildRowEntry(table, schema, row, buildOptions)) };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Reads a single row by primary key
|
|
552
|
+
*/
|
|
553
|
+
async readRow(table, pk) {
|
|
554
|
+
const schema = this.schemas.get(table);
|
|
555
|
+
if (!schema) return { message: `Table '${table}' not found` };
|
|
556
|
+
const buildOptions = { basePath: this.basePath };
|
|
557
|
+
const row = (await execAll$2(this.db, buildSelectByPK(table, schema, pk)))[0];
|
|
558
|
+
if (!row) return { message: `Row with pk '${pk}' not found in table '${table}'` };
|
|
559
|
+
return { data: buildRowEntry(table, schema, row, buildOptions) };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Gets table schema
|
|
563
|
+
*/
|
|
564
|
+
getSchema(table) {
|
|
565
|
+
const schema = this.schemas.get(table);
|
|
566
|
+
if (!schema) return { message: `Table '${table}' not found` };
|
|
567
|
+
return { data: buildSchemaEntry(table, schema, { basePath: this.basePath }) };
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Lists attributes (columns) for a row
|
|
571
|
+
*/
|
|
572
|
+
async listAttributes(table, pk) {
|
|
573
|
+
const schema = this.schemas.get(table);
|
|
574
|
+
if (!schema) return {
|
|
575
|
+
data: [],
|
|
576
|
+
message: `Table '${table}' not found`
|
|
577
|
+
};
|
|
578
|
+
const buildOptions = { basePath: this.basePath };
|
|
579
|
+
const row = (await execAll$2(this.db, buildSelectByPK(table, schema, pk)))[0];
|
|
580
|
+
if (!row) return {
|
|
581
|
+
data: [],
|
|
582
|
+
message: `Row with pk '${pk}' not found`
|
|
583
|
+
};
|
|
584
|
+
return { data: buildAttributeListEntry(table, schema, pk, row, buildOptions) };
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Gets a single attribute (column value) for a row
|
|
588
|
+
*/
|
|
589
|
+
async getAttribute(table, pk, column) {
|
|
590
|
+
const schema = this.schemas.get(table);
|
|
591
|
+
if (!schema) return { message: `Table '${table}' not found` };
|
|
592
|
+
if (!schema.columns.find((c) => c.name === column)) return { message: `Column '${column}' not found in table '${table}'` };
|
|
593
|
+
const buildOptions = { basePath: this.basePath };
|
|
594
|
+
const rows = await execAll$2(this.db, buildSelectByPK(table, schema, pk));
|
|
595
|
+
if (rows.length === 0) return { message: `Row with pk '${pk}' not found` };
|
|
596
|
+
return { data: buildAttributeEntry(table, pk, column, rows[0]?.[column], buildOptions) };
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Gets row metadata
|
|
600
|
+
*/
|
|
601
|
+
async getMeta(table, pk) {
|
|
602
|
+
const schema = this.schemas.get(table);
|
|
603
|
+
if (!schema) return { message: `Table '${table}' not found` };
|
|
604
|
+
const buildOptions = { basePath: this.basePath };
|
|
605
|
+
const row = (await execAll$2(this.db, buildSelectByPK(table, schema, pk)))[0];
|
|
606
|
+
if (!row) return { message: `Row with pk '${pk}' not found` };
|
|
607
|
+
return { data: buildMetaEntry(table, schema, pk, row, buildOptions) };
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Creates a new row in a table
|
|
611
|
+
*/
|
|
612
|
+
async createRow(table, content) {
|
|
613
|
+
const schema = this.schemas.get(table);
|
|
614
|
+
if (!schema) throw new Error(`Table '${table}' not found`);
|
|
615
|
+
const buildOptions = { basePath: this.basePath };
|
|
616
|
+
await execRun$1(this.db, buildInsert(table, schema, content));
|
|
617
|
+
const lastId = (await execAll$2(this.db, buildGetLastRowId()))[0]?.id;
|
|
618
|
+
if (lastId === void 0) throw new Error("Failed to get last inserted row ID");
|
|
619
|
+
const pkColumn = schema.primaryKey[0] ?? "rowid";
|
|
620
|
+
const pk = content[pkColumn] !== void 0 ? String(content[pkColumn]) : String(lastId);
|
|
621
|
+
const row = (await execAll$2(this.db, buildSelectByPK(table, schema, pk)))[0];
|
|
622
|
+
if (!row) throw new Error("Failed to fetch inserted row");
|
|
623
|
+
return { data: buildRowEntry(table, schema, row, buildOptions) };
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Updates an existing row
|
|
627
|
+
*/
|
|
628
|
+
async updateRow(table, pk, content) {
|
|
629
|
+
const schema = this.schemas.get(table);
|
|
630
|
+
if (!schema) throw new Error(`Table '${table}' not found`);
|
|
631
|
+
const buildOptions = { basePath: this.basePath };
|
|
632
|
+
await execRun$1(this.db, buildUpdate(table, schema, pk, content));
|
|
633
|
+
const row = (await execAll$2(this.db, buildSelectByPK(table, schema, pk)))[0];
|
|
634
|
+
if (!row) throw new Error(`Row with pk '${pk}' not found after update`);
|
|
635
|
+
return { data: buildRowEntry(table, schema, row, buildOptions) };
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Deletes a row by primary key
|
|
639
|
+
*/
|
|
640
|
+
async deleteRow(table, pk) {
|
|
641
|
+
const schema = this.schemas.get(table);
|
|
642
|
+
if (!schema) throw new Error(`Table '${table}' not found`);
|
|
643
|
+
if ((await execAll$2(this.db, buildSelectByPK(table, schema, pk))).length === 0) return { message: `Row with pk '${pk}' not found in table '${table}'` };
|
|
644
|
+
await execRun$1(this.db, buildDelete(table, schema, pk));
|
|
645
|
+
return { message: `Deleted row '${pk}' from table '${table}'` };
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Checks if a table exists
|
|
649
|
+
*/
|
|
650
|
+
hasTable(table) {
|
|
651
|
+
return this.schemas.has(table);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Gets the schema for a table
|
|
655
|
+
*/
|
|
656
|
+
getTableSchema(table) {
|
|
657
|
+
return this.schemas.get(table);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Updates the schemas map (after refresh)
|
|
661
|
+
*/
|
|
662
|
+
setSchemas(schemas) {
|
|
663
|
+
this.schemas = schemas;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/operations/search.ts
|
|
669
|
+
/**
|
|
670
|
+
* Executes a raw SQL query and returns all rows
|
|
671
|
+
*/
|
|
672
|
+
async function execAll$1(db, query) {
|
|
673
|
+
return db.all(sql.raw(query)).execute();
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* FTS5 Search operations for SQLite AFS
|
|
677
|
+
*/
|
|
678
|
+
var FTSSearch = class {
|
|
679
|
+
constructor(db, schemas, config, basePath = "") {
|
|
680
|
+
this.db = db;
|
|
681
|
+
this.schemas = schemas;
|
|
682
|
+
this.config = config;
|
|
683
|
+
this.basePath = basePath;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Performs full-text search across configured tables
|
|
687
|
+
*/
|
|
688
|
+
async search(query, options) {
|
|
689
|
+
if (!this.config.enabled) return {
|
|
690
|
+
data: [],
|
|
691
|
+
message: "Full-text search is not enabled"
|
|
692
|
+
};
|
|
693
|
+
const results = [];
|
|
694
|
+
const limit = options?.limit ?? 50;
|
|
695
|
+
const buildOptions = { basePath: this.basePath };
|
|
696
|
+
const tablesToSearch = options?.tables ? options.tables.filter((t) => this.config.tables.has(t)) : Array.from(this.config.tables.keys());
|
|
697
|
+
const ftsQuery = this.prepareFTSQuery(query, options?.caseSensitive);
|
|
698
|
+
for (const tableName of tablesToSearch) {
|
|
699
|
+
const tableConfig = this.config.tables.get(tableName);
|
|
700
|
+
const schema = this.schemas.get(tableName);
|
|
701
|
+
if (!tableConfig || !schema) continue;
|
|
702
|
+
const ftsTableName = `${tableName}_fts`;
|
|
703
|
+
try {
|
|
704
|
+
if (!await this.ftsTableExists(ftsTableName)) continue;
|
|
705
|
+
const highlightColumn = tableConfig.columns[0] ?? "";
|
|
706
|
+
const highlightIndex = highlightColumn ? tableConfig.columns.indexOf(highlightColumn) : 0;
|
|
707
|
+
const rows = await execAll$1(this.db, `
|
|
708
|
+
SELECT t.*, highlight("${ftsTableName}", ${highlightIndex}, '<mark>', '</mark>') as snippet
|
|
709
|
+
FROM "${ftsTableName}" fts
|
|
710
|
+
JOIN "${tableName}" t ON fts.rowid = t.rowid
|
|
711
|
+
WHERE "${ftsTableName}" MATCH '${ftsQuery}'
|
|
712
|
+
LIMIT ${Math.ceil(limit / tablesToSearch.length)}
|
|
713
|
+
`);
|
|
714
|
+
for (const row of rows) {
|
|
715
|
+
const { snippet, ...rowData } = row;
|
|
716
|
+
results.push(buildSearchEntry(tableName, schema, rowData, snippet, buildOptions));
|
|
717
|
+
}
|
|
718
|
+
} catch (error) {
|
|
719
|
+
console.warn(`FTS search failed for table ${tableName}:`, error);
|
|
720
|
+
}
|
|
721
|
+
if (results.length >= limit) break;
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
data: results.slice(0, limit),
|
|
725
|
+
message: results.length === 0 ? `No results found for "${query}"` : void 0
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Searches within a specific table
|
|
730
|
+
*/
|
|
731
|
+
async searchTable(tableName, query, options) {
|
|
732
|
+
return this.search(query, {
|
|
733
|
+
...options,
|
|
734
|
+
tables: [tableName]
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Checks if FTS is configured for a table
|
|
739
|
+
*/
|
|
740
|
+
hasFTS(tableName) {
|
|
741
|
+
return this.config.enabled && this.config.tables.has(tableName);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Gets FTS configuration for a table
|
|
745
|
+
*/
|
|
746
|
+
getFTSConfig(tableName) {
|
|
747
|
+
return this.config.tables.get(tableName);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Checks if an FTS table exists
|
|
751
|
+
*/
|
|
752
|
+
async ftsTableExists(ftsTableName) {
|
|
753
|
+
return (await execAll$1(this.db, `SELECT name FROM sqlite_master WHERE type = 'table' AND name = '${ftsTableName}'`)).length > 0;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Prepares a query string for FTS5
|
|
757
|
+
* Handles special characters and case sensitivity
|
|
758
|
+
*/
|
|
759
|
+
prepareFTSQuery(query, _caseSensitive) {
|
|
760
|
+
let prepared = query.replace(/"/g, "\"\"").replace(/'/g, "''");
|
|
761
|
+
if (prepared.includes(" ") && !prepared.startsWith("\"")) prepared = `"${prepared}"`;
|
|
762
|
+
return prepared;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Updates the schemas map (after refresh)
|
|
766
|
+
*/
|
|
767
|
+
setSchemas(schemas) {
|
|
768
|
+
this.schemas = schemas;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Simple search fallback when FTS is not available
|
|
772
|
+
* Uses LIKE queries on specified columns
|
|
773
|
+
*/
|
|
774
|
+
async simpleLikeSearch(tableName, query, columns, options) {
|
|
775
|
+
const schema = this.schemas.get(tableName);
|
|
776
|
+
if (!schema) return {
|
|
777
|
+
data: [],
|
|
778
|
+
message: `Table '${tableName}' not found`
|
|
779
|
+
};
|
|
780
|
+
const buildOptions = { basePath: this.basePath };
|
|
781
|
+
const limit = options?.limit ?? 50;
|
|
782
|
+
const escapedQuery = query.replace(/'/g, "''");
|
|
783
|
+
const conditions = columns.filter((col) => schema.columns.some((c) => c.name === col)).map((col) => `"${col}" LIKE '%${escapedQuery}%'`).join(" OR ");
|
|
784
|
+
if (!conditions) return {
|
|
785
|
+
data: [],
|
|
786
|
+
message: "No valid columns to search"
|
|
787
|
+
};
|
|
788
|
+
return { data: (await execAll$1(this.db, `SELECT * FROM "${tableName}" WHERE ${conditions} LIMIT ${limit}`)).map((row) => buildSearchEntry(tableName, schema, row, void 0, buildOptions)) };
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* Creates FTS configuration from options
|
|
793
|
+
*/
|
|
794
|
+
function createFTSConfig(options) {
|
|
795
|
+
const config = {
|
|
796
|
+
enabled: options?.enabled ?? false,
|
|
797
|
+
tables: /* @__PURE__ */ new Map()
|
|
798
|
+
};
|
|
799
|
+
if (options?.tables) for (const [table, columns] of Object.entries(options.tables)) config.tables.set(table, { columns });
|
|
800
|
+
return config;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/router/path-router.ts
|
|
805
|
+
/**
|
|
806
|
+
* Creates a radix3 router for SQLite AFS path routing
|
|
807
|
+
*
|
|
808
|
+
* Routes:
|
|
809
|
+
* - / → listTables
|
|
810
|
+
* - /:table → listTable
|
|
811
|
+
* - /:table/new → createRow
|
|
812
|
+
* - /:table/@schema → getSchema
|
|
813
|
+
* - /:table/:pk → readRow
|
|
814
|
+
* - /:table/:pk/@attr → listAttributes
|
|
815
|
+
* - /:table/:pk/@attr/:column → getAttribute
|
|
816
|
+
* - /:table/:pk/@meta → getMeta
|
|
817
|
+
* - /:table/:pk/@actions → listActions
|
|
818
|
+
* - /:table/:pk/@actions/:action → executeAction
|
|
819
|
+
*/
|
|
820
|
+
function createPathRouter() {
|
|
821
|
+
return createRouter({ routes: {
|
|
822
|
+
"/": { action: "listTables" },
|
|
823
|
+
"/:table": { action: "listTable" },
|
|
824
|
+
"/:table/new": { action: "createRow" },
|
|
825
|
+
"/:table/@schema": { action: "getSchema" },
|
|
826
|
+
"/:table/:pk": { action: "readRow" },
|
|
827
|
+
"/:table/:pk/@attr": { action: "listAttributes" },
|
|
828
|
+
"/:table/:pk/@attr/:column": { action: "getAttribute" },
|
|
829
|
+
"/:table/:pk/@meta": { action: "getMeta" },
|
|
830
|
+
"/:table/:pk/@actions": { action: "listActions" },
|
|
831
|
+
"/:table/:pk/@actions/:action": { action: "executeAction" }
|
|
832
|
+
} });
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Parses a path and returns the matched route with params
|
|
836
|
+
* @param router - The radix3 router instance
|
|
837
|
+
* @param path - The path to match
|
|
838
|
+
* @returns RouteMatch if matched, undefined otherwise
|
|
839
|
+
*/
|
|
840
|
+
function matchPath(router, path) {
|
|
841
|
+
const result = router.lookup(path);
|
|
842
|
+
if (!result) return void 0;
|
|
843
|
+
return {
|
|
844
|
+
action: result.action,
|
|
845
|
+
params: result.params ?? {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Builds a path from components
|
|
850
|
+
*/
|
|
851
|
+
function buildPath(table, pk, suffix) {
|
|
852
|
+
const parts = ["/"];
|
|
853
|
+
if (table) parts.push(table);
|
|
854
|
+
if (pk) parts.push(pk);
|
|
855
|
+
if (suffix) parts.push(suffix);
|
|
856
|
+
return parts.join("/").replace(/\/+/g, "/");
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Checks if a path segment is a virtual path (@attr, @meta, @actions, @schema)
|
|
860
|
+
*/
|
|
861
|
+
function isVirtualPath(segment) {
|
|
862
|
+
return segment.startsWith("@");
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Gets the type of virtual path
|
|
866
|
+
*/
|
|
867
|
+
function getVirtualPathType(segment) {
|
|
868
|
+
if (segment === "@attr") return "attr";
|
|
869
|
+
if (segment === "@meta") return "meta";
|
|
870
|
+
if (segment === "@actions") return "actions";
|
|
871
|
+
if (segment === "@schema") return "schema";
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/schema/types.ts
|
|
877
|
+
/**
|
|
878
|
+
* System tables that should be excluded from introspection
|
|
879
|
+
*/
|
|
880
|
+
const SYSTEM_TABLES = [
|
|
881
|
+
"sqlite_sequence",
|
|
882
|
+
"sqlite_stat1",
|
|
883
|
+
"sqlite_stat2",
|
|
884
|
+
"sqlite_stat3",
|
|
885
|
+
"sqlite_stat4"
|
|
886
|
+
];
|
|
887
|
+
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/schema/introspector.ts
|
|
890
|
+
/**
|
|
891
|
+
* Executes a raw SQL query and returns all rows
|
|
892
|
+
*/
|
|
893
|
+
async function execAll(db, query) {
|
|
894
|
+
return db.all(sql.raw(query)).execute();
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Executes a raw SQL query (for INSERT, UPDATE, DELETE)
|
|
898
|
+
*/
|
|
899
|
+
async function execRun(db, query) {
|
|
900
|
+
await db.run(sql.raw(query)).execute();
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Maps raw PRAGMA table_info row to ColumnInfo
|
|
904
|
+
*/
|
|
905
|
+
function mapColumn(row) {
|
|
906
|
+
return {
|
|
907
|
+
name: row.name,
|
|
908
|
+
type: row.type,
|
|
909
|
+
notnull: row.notnull === 1,
|
|
910
|
+
pk: row.pk,
|
|
911
|
+
dfltValue: row.dflt_value
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Maps raw PRAGMA foreign_key_list row to ForeignKeyInfo
|
|
916
|
+
*/
|
|
917
|
+
function mapForeignKey(row) {
|
|
918
|
+
return {
|
|
919
|
+
id: row.id,
|
|
920
|
+
seq: row.seq,
|
|
921
|
+
table: row.table,
|
|
922
|
+
from: row.from,
|
|
923
|
+
to: row.to,
|
|
924
|
+
onUpdate: row.on_update,
|
|
925
|
+
onDelete: row.on_delete,
|
|
926
|
+
match: row.match
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Maps raw PRAGMA index_list row to IndexInfo
|
|
931
|
+
*/
|
|
932
|
+
function mapIndex(row) {
|
|
933
|
+
return {
|
|
934
|
+
seq: row.seq,
|
|
935
|
+
name: row.name,
|
|
936
|
+
unique: row.unique === 1,
|
|
937
|
+
origin: row.origin,
|
|
938
|
+
partial: row.partial === 1
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Schema introspector that uses SQLite PRAGMA queries to discover database schema
|
|
943
|
+
*/
|
|
944
|
+
var SchemaIntrospector = class {
|
|
945
|
+
/**
|
|
946
|
+
* Introspects all tables in the database
|
|
947
|
+
* @param db - Drizzle database instance
|
|
948
|
+
* @param options - Introspection options
|
|
949
|
+
* @returns Map of table name to TableSchema
|
|
950
|
+
*/
|
|
951
|
+
async introspect(db, options) {
|
|
952
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
953
|
+
const tablesResult = await execAll(db, `
|
|
954
|
+
SELECT name FROM sqlite_master
|
|
955
|
+
WHERE type = 'table'
|
|
956
|
+
AND name NOT LIKE 'sqlite_%'
|
|
957
|
+
AND name NOT LIKE '%_fts%'
|
|
958
|
+
ORDER BY name
|
|
959
|
+
`);
|
|
960
|
+
for (const { name } of tablesResult) {
|
|
961
|
+
if (SYSTEM_TABLES.includes(name)) continue;
|
|
962
|
+
if (options?.tables && !options.tables.includes(name)) continue;
|
|
963
|
+
if (options?.excludeTables?.includes(name)) continue;
|
|
964
|
+
const schema = await this.introspectTable(db, name);
|
|
965
|
+
schemas.set(name, schema);
|
|
966
|
+
}
|
|
967
|
+
return schemas;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Introspects a single table
|
|
971
|
+
* @param db - Drizzle database instance
|
|
972
|
+
* @param tableName - Name of the table to introspect
|
|
973
|
+
* @returns TableSchema for the specified table
|
|
974
|
+
*/
|
|
975
|
+
async introspectTable(db, tableName) {
|
|
976
|
+
const columns = (await execAll(db, `PRAGMA table_info("${tableName}")`)).map(mapColumn);
|
|
977
|
+
return {
|
|
978
|
+
name: tableName,
|
|
979
|
+
columns,
|
|
980
|
+
primaryKey: columns.filter((c) => c.pk > 0).map((c) => c.name),
|
|
981
|
+
foreignKeys: (await execAll(db, `PRAGMA foreign_key_list("${tableName}")`)).map(mapForeignKey),
|
|
982
|
+
indexes: (await execAll(db, `PRAGMA index_list("${tableName}")`)).map(mapIndex)
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Gets the primary key column name for a table
|
|
987
|
+
* Returns the first PK column, or 'rowid' if no explicit PK
|
|
988
|
+
*/
|
|
989
|
+
getPrimaryKeyColumn(schema) {
|
|
990
|
+
if (schema.primaryKey.length > 0 && schema.primaryKey[0]) return schema.primaryKey[0];
|
|
991
|
+
return "rowid";
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Checks if a table has FTS (Full-Text Search) enabled
|
|
995
|
+
*/
|
|
996
|
+
async hasFTS(db, tableName) {
|
|
997
|
+
return (await execAll(db, `
|
|
998
|
+
SELECT name FROM sqlite_master
|
|
999
|
+
WHERE type = 'table'
|
|
1000
|
+
AND name = '${`${tableName}_fts`}'
|
|
1001
|
+
`)).length > 0;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Creates FTS5 table for full-text search on specified columns
|
|
1005
|
+
*/
|
|
1006
|
+
async createFTS(db, tableName, columns, options) {
|
|
1007
|
+
const ftsTableName = `${tableName}_fts`;
|
|
1008
|
+
const contentTable = options?.contentTable ?? tableName;
|
|
1009
|
+
const contentRowid = options?.contentRowid ?? "rowid";
|
|
1010
|
+
const columnList = columns.join(", ");
|
|
1011
|
+
await execRun(db, `
|
|
1012
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS "${ftsTableName}" USING fts5(
|
|
1013
|
+
${columnList},
|
|
1014
|
+
content="${contentTable}",
|
|
1015
|
+
content_rowid="${contentRowid}"
|
|
1016
|
+
)
|
|
1017
|
+
`);
|
|
1018
|
+
await execRun(db, `
|
|
1019
|
+
CREATE TRIGGER IF NOT EXISTS "${tableName}_ai" AFTER INSERT ON "${tableName}" BEGIN
|
|
1020
|
+
INSERT INTO "${ftsTableName}"(rowid, ${columnList}) VALUES (new.${contentRowid}, ${columns.map((c) => `new."${c}"`).join(", ")});
|
|
1021
|
+
END
|
|
1022
|
+
`);
|
|
1023
|
+
await execRun(db, `
|
|
1024
|
+
CREATE TRIGGER IF NOT EXISTS "${tableName}_ad" AFTER DELETE ON "${tableName}" BEGIN
|
|
1025
|
+
INSERT INTO "${ftsTableName}"("${ftsTableName}", rowid, ${columnList}) VALUES ('delete', old.${contentRowid}, ${columns.map((c) => `old."${c}"`).join(", ")});
|
|
1026
|
+
END
|
|
1027
|
+
`);
|
|
1028
|
+
await execRun(db, `
|
|
1029
|
+
CREATE TRIGGER IF NOT EXISTS "${tableName}_au" AFTER UPDATE ON "${tableName}" BEGIN
|
|
1030
|
+
INSERT INTO "${ftsTableName}"("${ftsTableName}", rowid, ${columnList}) VALUES ('delete', old.${contentRowid}, ${columns.map((c) => `old."${c}"`).join(", ")});
|
|
1031
|
+
INSERT INTO "${ftsTableName}"(rowid, ${columnList}) VALUES (new.${contentRowid}, ${columns.map((c) => `new."${c}"`).join(", ")});
|
|
1032
|
+
END
|
|
1033
|
+
`);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Rebuilds FTS index for a table (useful after bulk inserts)
|
|
1037
|
+
*/
|
|
1038
|
+
async rebuildFTS(db, tableName) {
|
|
1039
|
+
const ftsTableName = `${tableName}_fts`;
|
|
1040
|
+
await execRun(db, `INSERT INTO "${ftsTableName}"("${ftsTableName}") VALUES ('rebuild')`);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/sqlite-afs.ts
|
|
1046
|
+
/**
|
|
1047
|
+
* SQLite AFS Module
|
|
1048
|
+
*
|
|
1049
|
+
* Exposes SQLite databases as AFS nodes with full CRUD support,
|
|
1050
|
+
* schema introspection, FTS5 search, and virtual paths (@attr, @meta, @actions).
|
|
1051
|
+
*/
|
|
1052
|
+
var SQLiteAFS = class SQLiteAFS {
|
|
1053
|
+
name;
|
|
1054
|
+
description;
|
|
1055
|
+
accessMode;
|
|
1056
|
+
db;
|
|
1057
|
+
schemas = /* @__PURE__ */ new Map();
|
|
1058
|
+
router;
|
|
1059
|
+
crud;
|
|
1060
|
+
ftsSearch;
|
|
1061
|
+
actions;
|
|
1062
|
+
ftsConfig;
|
|
1063
|
+
initialized = false;
|
|
1064
|
+
constructor(options) {
|
|
1065
|
+
this.options = options;
|
|
1066
|
+
this.name = options.name ?? "sqlite-afs";
|
|
1067
|
+
this.description = options.description ?? `SQLite database: ${options.url}`;
|
|
1068
|
+
this.accessMode = options.accessMode ?? "readwrite";
|
|
1069
|
+
this.ftsConfig = createFTSConfig(options.fts);
|
|
1070
|
+
this.actions = new ActionsRegistry();
|
|
1071
|
+
registerBuiltInActions(this.actions);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Returns the Zod schema for configuration validation
|
|
1075
|
+
*/
|
|
1076
|
+
static schema() {
|
|
1077
|
+
return sqliteAFSConfigSchema;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Loads a module instance from configuration
|
|
1081
|
+
*/
|
|
1082
|
+
static async load({ parsed }) {
|
|
1083
|
+
return new SQLiteAFS(sqliteAFSConfigSchema.parse(parsed));
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Called when the module is mounted to AFS
|
|
1087
|
+
*/
|
|
1088
|
+
async onMount(_afs) {
|
|
1089
|
+
await this.initialize();
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Initializes the database connection and introspects schema
|
|
1093
|
+
*/
|
|
1094
|
+
async initialize() {
|
|
1095
|
+
if (this.initialized) return;
|
|
1096
|
+
this.db = await initDatabase({
|
|
1097
|
+
url: this.options.url,
|
|
1098
|
+
wal: this.options.wal ?? true
|
|
1099
|
+
});
|
|
1100
|
+
const db = this.db;
|
|
1101
|
+
this.schemas = await new SchemaIntrospector().introspect(db, {
|
|
1102
|
+
tables: this.options.tables,
|
|
1103
|
+
excludeTables: this.options.excludeTables
|
|
1104
|
+
});
|
|
1105
|
+
this.router = createPathRouter();
|
|
1106
|
+
this.crud = new CRUDOperations(db, this.schemas, "");
|
|
1107
|
+
this.ftsSearch = new FTSSearch(db, this.schemas, this.ftsConfig, "");
|
|
1108
|
+
this.initialized = true;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Ensures the module is initialized
|
|
1112
|
+
*/
|
|
1113
|
+
async ensureInitialized() {
|
|
1114
|
+
if (!this.initialized) await this.initialize();
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Lists entries at a path
|
|
1118
|
+
*/
|
|
1119
|
+
async list(path, options) {
|
|
1120
|
+
await this.ensureInitialized();
|
|
1121
|
+
const match = matchPath(this.router, path);
|
|
1122
|
+
if (!match) return { data: [] };
|
|
1123
|
+
switch (match.action) {
|
|
1124
|
+
case "listTables": return this.crud.listTables();
|
|
1125
|
+
case "listTable":
|
|
1126
|
+
if (!match.params.table) return { data: [] };
|
|
1127
|
+
return this.crud.listTable(match.params.table, options);
|
|
1128
|
+
case "listAttributes":
|
|
1129
|
+
if (!match.params.table || !match.params.pk) return { data: [] };
|
|
1130
|
+
return this.crud.listAttributes(match.params.table, match.params.pk);
|
|
1131
|
+
case "listActions":
|
|
1132
|
+
if (!match.params.table || !match.params.pk) return { data: [] };
|
|
1133
|
+
return this.listActions(match.params.table, match.params.pk);
|
|
1134
|
+
default: return { data: [] };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Reads an entry at a path
|
|
1139
|
+
*/
|
|
1140
|
+
async read(path, _options) {
|
|
1141
|
+
await this.ensureInitialized();
|
|
1142
|
+
const match = matchPath(this.router, path);
|
|
1143
|
+
if (!match) return {};
|
|
1144
|
+
switch (match.action) {
|
|
1145
|
+
case "readRow":
|
|
1146
|
+
if (!match.params.table || !match.params.pk) return {};
|
|
1147
|
+
return this.crud.readRow(match.params.table, match.params.pk);
|
|
1148
|
+
case "getSchema":
|
|
1149
|
+
if (!match.params.table) return {};
|
|
1150
|
+
return this.crud.getSchema(match.params.table);
|
|
1151
|
+
case "getAttribute":
|
|
1152
|
+
if (!match.params.table || !match.params.pk || !match.params.column) return {};
|
|
1153
|
+
return this.crud.getAttribute(match.params.table, match.params.pk, match.params.column);
|
|
1154
|
+
case "getMeta":
|
|
1155
|
+
if (!match.params.table || !match.params.pk) return {};
|
|
1156
|
+
return this.crud.getMeta(match.params.table, match.params.pk);
|
|
1157
|
+
default: return {};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Writes an entry at a path
|
|
1162
|
+
*/
|
|
1163
|
+
async write(path, content, _options) {
|
|
1164
|
+
await this.ensureInitialized();
|
|
1165
|
+
if (this.accessMode === "readonly") throw new Error("Module is in readonly mode");
|
|
1166
|
+
const match = matchPath(this.router, path);
|
|
1167
|
+
if (!match) throw new Error(`Invalid path: ${path}`);
|
|
1168
|
+
switch (match.action) {
|
|
1169
|
+
case "createRow":
|
|
1170
|
+
if (!match.params.table) throw new Error("Table name required for create");
|
|
1171
|
+
return this.crud.createRow(match.params.table, content.content ?? content);
|
|
1172
|
+
case "readRow":
|
|
1173
|
+
if (!match.params.table || !match.params.pk) throw new Error("Table and primary key required for update");
|
|
1174
|
+
return this.crud.updateRow(match.params.table, match.params.pk, content.content ?? content);
|
|
1175
|
+
case "executeAction":
|
|
1176
|
+
if (!match.params.table || !match.params.action) throw new Error("Table and action name required");
|
|
1177
|
+
return this.executeAction(match.params.table, match.params.pk, match.params.action, content.content ?? content);
|
|
1178
|
+
default: throw new Error(`Write not supported for path: ${path}`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Deletes an entry at a path
|
|
1183
|
+
*/
|
|
1184
|
+
async delete(path, _options) {
|
|
1185
|
+
await this.ensureInitialized();
|
|
1186
|
+
if (this.accessMode === "readonly") throw new Error("Module is in readonly mode");
|
|
1187
|
+
const match = matchPath(this.router, path);
|
|
1188
|
+
if (!match || match.action !== "readRow") throw new Error(`Delete not supported for path: ${path}`);
|
|
1189
|
+
if (!match.params.table || !match.params.pk) throw new Error("Table and primary key required for delete");
|
|
1190
|
+
return this.crud.deleteRow(match.params.table, match.params.pk);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Searches for entries matching a query
|
|
1194
|
+
*/
|
|
1195
|
+
async search(path, query, options) {
|
|
1196
|
+
await this.ensureInitialized();
|
|
1197
|
+
const match = matchPath(this.router, path);
|
|
1198
|
+
if (match?.params.table) return this.ftsSearch.searchTable(match.params.table, query, options);
|
|
1199
|
+
return this.ftsSearch.search(query, options);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Executes a module operation
|
|
1203
|
+
*/
|
|
1204
|
+
async exec(path, args, _options) {
|
|
1205
|
+
await this.ensureInitialized();
|
|
1206
|
+
const match = matchPath(this.router, path);
|
|
1207
|
+
if (match?.action === "executeAction" && match.params.table && match.params.action) return { data: (await this.executeAction(match.params.table, match.params.pk, match.params.action, args)).data };
|
|
1208
|
+
throw new Error(`Exec not supported for path: ${path}`);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Lists available actions for a row
|
|
1212
|
+
*/
|
|
1213
|
+
listActions(table, pk) {
|
|
1214
|
+
return { data: buildActionsListEntry(table, pk, this.actions.listNames({ rowLevel: true }), { basePath: "" }) };
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Executes an action
|
|
1218
|
+
*/
|
|
1219
|
+
async executeAction(table, pk, actionName, params) {
|
|
1220
|
+
if (!this.schemas.get(table)) throw new Error(`Table '${table}' not found`);
|
|
1221
|
+
let row;
|
|
1222
|
+
if (pk) row = (await this.crud.readRow(table, pk)).data?.content;
|
|
1223
|
+
const ctx = {
|
|
1224
|
+
db: this.db,
|
|
1225
|
+
schemas: this.schemas,
|
|
1226
|
+
table,
|
|
1227
|
+
pk,
|
|
1228
|
+
row,
|
|
1229
|
+
module: {
|
|
1230
|
+
refreshSchema: () => this.refreshSchema(),
|
|
1231
|
+
exportTable: (t, f) => this.exportTable(t, f)
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
const result = await this.actions.execute(actionName, ctx, params);
|
|
1235
|
+
if (!result.success) throw new Error(result.message ?? "Action failed");
|
|
1236
|
+
return { data: {
|
|
1237
|
+
id: `${table}:${pk ?? ""}:@actions:${actionName}`,
|
|
1238
|
+
path: pk ? `/${table}/${pk}/@actions/${actionName}` : `/${table}/@actions/${actionName}`,
|
|
1239
|
+
content: result.data
|
|
1240
|
+
} };
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Refreshes the schema cache
|
|
1244
|
+
*/
|
|
1245
|
+
async refreshSchema() {
|
|
1246
|
+
const db = this.db;
|
|
1247
|
+
this.schemas = await new SchemaIntrospector().introspect(db, {
|
|
1248
|
+
tables: this.options.tables,
|
|
1249
|
+
excludeTables: this.options.excludeTables
|
|
1250
|
+
});
|
|
1251
|
+
this.crud.setSchemas(this.schemas);
|
|
1252
|
+
this.ftsSearch.setSchemas(this.schemas);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Exports table data in specified format
|
|
1256
|
+
*/
|
|
1257
|
+
async exportTable(table, format) {
|
|
1258
|
+
const listResult = await this.crud.listTable(table, { limit: 1e4 });
|
|
1259
|
+
if (format === "csv") {
|
|
1260
|
+
const schema = this.schemas.get(table);
|
|
1261
|
+
if (!schema) throw new Error(`Table '${table}' not found`);
|
|
1262
|
+
return `${schema.columns.map((c) => c.name).join(",")}\n${listResult.data.map((entry) => {
|
|
1263
|
+
const content = entry.content;
|
|
1264
|
+
return schema.columns.map((c) => {
|
|
1265
|
+
const val = content[c.name];
|
|
1266
|
+
if (val === null || val === void 0) return "";
|
|
1267
|
+
if (typeof val === "string" && (val.includes(",") || val.includes("\""))) return `"${val.replace(/"/g, "\"\"")}"`;
|
|
1268
|
+
return String(val);
|
|
1269
|
+
}).join(",");
|
|
1270
|
+
}).join("\n")}`;
|
|
1271
|
+
}
|
|
1272
|
+
return listResult.data.map((entry) => entry.content);
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Registers a custom action
|
|
1276
|
+
*/
|
|
1277
|
+
registerAction(name, handler, options) {
|
|
1278
|
+
this.actions.registerSimple(name, async (ctx, params) => ({
|
|
1279
|
+
success: true,
|
|
1280
|
+
data: await handler(ctx, params)
|
|
1281
|
+
}), options);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Gets table schemas (for external access)
|
|
1285
|
+
*/
|
|
1286
|
+
getSchemas() {
|
|
1287
|
+
return this.schemas;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Gets the database instance (for advanced operations)
|
|
1291
|
+
*/
|
|
1292
|
+
getDatabase() {
|
|
1293
|
+
return this.db;
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
//#endregion
|
|
1298
|
+
export { ActionsRegistry, CRUDOperations, FTSSearch, SQLiteAFS, SchemaIntrospector, buildActionsListEntry, buildAttributeEntry, buildAttributeListEntry, buildDelete, buildGetLastRowId, buildInsert, buildMetaEntry, buildPath, buildRowEntry, buildSchemaEntry, buildSearchEntry, buildSelectAll, buildSelectByPK, buildTableEntry, buildUpdate, createFTSConfig, createPathRouter, getVirtualPathType, isVirtualPath, matchPath, registerBuiltInActions, sqliteAFSConfigSchema };
|
|
1299
|
+
//# sourceMappingURL=index.mjs.map
|