@aigne/afs-sqlite 1.0.1-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.
Files changed (76) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE.md +93 -0
  3. package/README.md +277 -0
  4. package/lib/cjs/actions/built-in.d.ts +5 -0
  5. package/lib/cjs/actions/built-in.js +165 -0
  6. package/lib/cjs/actions/registry.d.ts +49 -0
  7. package/lib/cjs/actions/registry.js +102 -0
  8. package/lib/cjs/actions/types.d.ts +51 -0
  9. package/lib/cjs/actions/types.js +2 -0
  10. package/lib/cjs/config.d.ts +89 -0
  11. package/lib/cjs/config.js +33 -0
  12. package/lib/cjs/index.d.ts +13 -0
  13. package/lib/cjs/index.js +47 -0
  14. package/lib/cjs/node/builder.d.ts +43 -0
  15. package/lib/cjs/node/builder.js +187 -0
  16. package/lib/cjs/operations/crud.d.ts +64 -0
  17. package/lib/cjs/operations/crud.js +225 -0
  18. package/lib/cjs/operations/query-builder.d.ts +37 -0
  19. package/lib/cjs/operations/query-builder.js +102 -0
  20. package/lib/cjs/operations/search.d.ts +75 -0
  21. package/lib/cjs/operations/search.js +172 -0
  22. package/lib/cjs/package.json +3 -0
  23. package/lib/cjs/router/path-router.d.ts +38 -0
  24. package/lib/cjs/router/path-router.js +90 -0
  25. package/lib/cjs/router/types.d.ts +30 -0
  26. package/lib/cjs/router/types.js +2 -0
  27. package/lib/cjs/schema/introspector.d.ts +48 -0
  28. package/lib/cjs/schema/introspector.js +186 -0
  29. package/lib/cjs/schema/types.d.ts +104 -0
  30. package/lib/cjs/schema/types.js +13 -0
  31. package/lib/cjs/sqlite-afs.d.ts +144 -0
  32. package/lib/cjs/sqlite-afs.js +337 -0
  33. package/lib/dts/actions/built-in.d.ts +5 -0
  34. package/lib/dts/actions/registry.d.ts +49 -0
  35. package/lib/dts/actions/types.d.ts +51 -0
  36. package/lib/dts/config.d.ts +89 -0
  37. package/lib/dts/index.d.ts +13 -0
  38. package/lib/dts/node/builder.d.ts +43 -0
  39. package/lib/dts/operations/crud.d.ts +64 -0
  40. package/lib/dts/operations/query-builder.d.ts +37 -0
  41. package/lib/dts/operations/search.d.ts +75 -0
  42. package/lib/dts/router/path-router.d.ts +38 -0
  43. package/lib/dts/router/types.d.ts +30 -0
  44. package/lib/dts/schema/introspector.d.ts +48 -0
  45. package/lib/dts/schema/types.d.ts +104 -0
  46. package/lib/dts/sqlite-afs.d.ts +144 -0
  47. package/lib/esm/actions/built-in.d.ts +5 -0
  48. package/lib/esm/actions/built-in.js +162 -0
  49. package/lib/esm/actions/registry.d.ts +49 -0
  50. package/lib/esm/actions/registry.js +98 -0
  51. package/lib/esm/actions/types.d.ts +51 -0
  52. package/lib/esm/actions/types.js +1 -0
  53. package/lib/esm/config.d.ts +89 -0
  54. package/lib/esm/config.js +30 -0
  55. package/lib/esm/index.d.ts +13 -0
  56. package/lib/esm/index.js +17 -0
  57. package/lib/esm/node/builder.d.ts +43 -0
  58. package/lib/esm/node/builder.js +177 -0
  59. package/lib/esm/operations/crud.d.ts +64 -0
  60. package/lib/esm/operations/crud.js +221 -0
  61. package/lib/esm/operations/query-builder.d.ts +37 -0
  62. package/lib/esm/operations/query-builder.js +92 -0
  63. package/lib/esm/operations/search.d.ts +75 -0
  64. package/lib/esm/operations/search.js +167 -0
  65. package/lib/esm/package.json +3 -0
  66. package/lib/esm/router/path-router.d.ts +38 -0
  67. package/lib/esm/router/path-router.js +83 -0
  68. package/lib/esm/router/types.d.ts +30 -0
  69. package/lib/esm/router/types.js +1 -0
  70. package/lib/esm/schema/introspector.d.ts +48 -0
  71. package/lib/esm/schema/introspector.js +182 -0
  72. package/lib/esm/schema/types.d.ts +104 -0
  73. package/lib/esm/schema/types.js +10 -0
  74. package/lib/esm/sqlite-afs.d.ts +144 -0
  75. package/lib/esm/sqlite-afs.js +333 -0
  76. package/package.json +71 -0
@@ -0,0 +1,333 @@
1
+ import { initDatabase } from "@aigne/sqlite";
2
+ import { registerBuiltInActions } from "./actions/built-in.js";
3
+ import { ActionsRegistry } from "./actions/registry.js";
4
+ import { sqliteAFSConfigSchema } from "./config.js";
5
+ import { buildActionsListEntry } from "./node/builder.js";
6
+ import { CRUDOperations } from "./operations/crud.js";
7
+ import { createFTSConfig, FTSSearch } from "./operations/search.js";
8
+ import { createPathRouter, matchPath } from "./router/path-router.js";
9
+ import { SchemaIntrospector } from "./schema/introspector.js";
10
+ /**
11
+ * SQLite AFS Module
12
+ *
13
+ * Exposes SQLite databases as AFS nodes with full CRUD support,
14
+ * schema introspection, FTS5 search, and virtual paths (@attr, @meta, @actions).
15
+ */
16
+ export class SQLiteAFS {
17
+ options;
18
+ name;
19
+ description;
20
+ accessMode;
21
+ db;
22
+ schemas = new Map();
23
+ router;
24
+ crud;
25
+ ftsSearch;
26
+ actions;
27
+ ftsConfig;
28
+ initialized = false;
29
+ constructor(options) {
30
+ this.options = options;
31
+ this.name = options.name ?? "sqlite-afs";
32
+ this.description = options.description ?? `SQLite database: ${options.url}`;
33
+ this.accessMode = options.accessMode ?? "readwrite";
34
+ this.ftsConfig = createFTSConfig(options.fts);
35
+ this.actions = new ActionsRegistry();
36
+ registerBuiltInActions(this.actions);
37
+ }
38
+ /**
39
+ * Returns the Zod schema for configuration validation
40
+ */
41
+ static schema() {
42
+ return sqliteAFSConfigSchema;
43
+ }
44
+ /**
45
+ * Loads a module instance from configuration
46
+ */
47
+ static async load({ parsed }) {
48
+ const validated = sqliteAFSConfigSchema.parse(parsed);
49
+ return new SQLiteAFS(validated);
50
+ }
51
+ /**
52
+ * Called when the module is mounted to AFS
53
+ */
54
+ async onMount(_afs) {
55
+ await this.initialize();
56
+ }
57
+ /**
58
+ * Initializes the database connection and introspects schema
59
+ */
60
+ async initialize() {
61
+ if (this.initialized)
62
+ return;
63
+ // Initialize database connection
64
+ this.db = await initDatabase({
65
+ url: this.options.url,
66
+ wal: this.options.wal ?? true,
67
+ });
68
+ // Cast db to LibSQLDatabase for operations
69
+ const db = this.db;
70
+ // Introspect database schema
71
+ const introspector = new SchemaIntrospector();
72
+ this.schemas = await introspector.introspect(db, {
73
+ tables: this.options.tables,
74
+ excludeTables: this.options.excludeTables,
75
+ });
76
+ // Initialize components
77
+ this.router = createPathRouter();
78
+ this.crud = new CRUDOperations(db, this.schemas, "");
79
+ this.ftsSearch = new FTSSearch(db, this.schemas, this.ftsConfig, "");
80
+ this.initialized = true;
81
+ }
82
+ /**
83
+ * Ensures the module is initialized
84
+ */
85
+ async ensureInitialized() {
86
+ if (!this.initialized) {
87
+ await this.initialize();
88
+ }
89
+ }
90
+ /**
91
+ * Lists entries at a path
92
+ */
93
+ async list(path, options) {
94
+ await this.ensureInitialized();
95
+ const match = matchPath(this.router, path);
96
+ if (!match) {
97
+ return { data: [] };
98
+ }
99
+ switch (match.action) {
100
+ case "listTables":
101
+ return this.crud.listTables();
102
+ case "listTable":
103
+ if (!match.params.table)
104
+ return { data: [] };
105
+ return this.crud.listTable(match.params.table, options);
106
+ case "listAttributes":
107
+ if (!match.params.table || !match.params.pk)
108
+ return { data: [] };
109
+ return this.crud.listAttributes(match.params.table, match.params.pk);
110
+ case "listActions":
111
+ if (!match.params.table || !match.params.pk)
112
+ return { data: [] };
113
+ return this.listActions(match.params.table, match.params.pk);
114
+ default:
115
+ return { data: [] };
116
+ }
117
+ }
118
+ /**
119
+ * Reads an entry at a path
120
+ */
121
+ async read(path, _options) {
122
+ await this.ensureInitialized();
123
+ const match = matchPath(this.router, path);
124
+ if (!match) {
125
+ return {};
126
+ }
127
+ switch (match.action) {
128
+ case "readRow":
129
+ if (!match.params.table || !match.params.pk)
130
+ return {};
131
+ return this.crud.readRow(match.params.table, match.params.pk);
132
+ case "getSchema":
133
+ if (!match.params.table)
134
+ return {};
135
+ return this.crud.getSchema(match.params.table);
136
+ case "getAttribute":
137
+ if (!match.params.table || !match.params.pk || !match.params.column)
138
+ return {};
139
+ return this.crud.getAttribute(match.params.table, match.params.pk, match.params.column);
140
+ case "getMeta":
141
+ if (!match.params.table || !match.params.pk)
142
+ return {};
143
+ return this.crud.getMeta(match.params.table, match.params.pk);
144
+ default:
145
+ return {};
146
+ }
147
+ }
148
+ /**
149
+ * Writes an entry at a path
150
+ */
151
+ async write(path, content, _options) {
152
+ await this.ensureInitialized();
153
+ if (this.accessMode === "readonly") {
154
+ throw new Error("Module is in readonly mode");
155
+ }
156
+ const match = matchPath(this.router, path);
157
+ if (!match) {
158
+ throw new Error(`Invalid path: ${path}`);
159
+ }
160
+ switch (match.action) {
161
+ case "createRow":
162
+ if (!match.params.table) {
163
+ throw new Error("Table name required for create");
164
+ }
165
+ return this.crud.createRow(match.params.table, content.content ?? content);
166
+ case "readRow":
167
+ if (!match.params.table || !match.params.pk) {
168
+ throw new Error("Table and primary key required for update");
169
+ }
170
+ return this.crud.updateRow(match.params.table, match.params.pk, content.content ?? content);
171
+ case "executeAction":
172
+ if (!match.params.table || !match.params.action) {
173
+ throw new Error("Table and action name required");
174
+ }
175
+ return this.executeAction(match.params.table, match.params.pk, match.params.action, content.content ?? content);
176
+ default:
177
+ throw new Error(`Write not supported for path: ${path}`);
178
+ }
179
+ }
180
+ /**
181
+ * Deletes an entry at a path
182
+ */
183
+ async delete(path, _options) {
184
+ await this.ensureInitialized();
185
+ if (this.accessMode === "readonly") {
186
+ throw new Error("Module is in readonly mode");
187
+ }
188
+ const match = matchPath(this.router, path);
189
+ if (!match || match.action !== "readRow") {
190
+ throw new Error(`Delete not supported for path: ${path}`);
191
+ }
192
+ if (!match.params.table || !match.params.pk) {
193
+ throw new Error("Table and primary key required for delete");
194
+ }
195
+ return this.crud.deleteRow(match.params.table, match.params.pk);
196
+ }
197
+ /**
198
+ * Searches for entries matching a query
199
+ */
200
+ async search(path, query, options) {
201
+ await this.ensureInitialized();
202
+ // If path specifies a table, search only that table
203
+ const match = matchPath(this.router, path);
204
+ if (match?.params.table) {
205
+ return this.ftsSearch.searchTable(match.params.table, query, options);
206
+ }
207
+ // Otherwise search all FTS-enabled tables
208
+ return this.ftsSearch.search(query, options);
209
+ }
210
+ /**
211
+ * Executes a module operation
212
+ */
213
+ async exec(path, args, _options) {
214
+ await this.ensureInitialized();
215
+ const match = matchPath(this.router, path);
216
+ if (match?.action === "executeAction" && match.params.table && match.params.action) {
217
+ const result = await this.executeAction(match.params.table, match.params.pk, match.params.action, args);
218
+ return { data: result.data };
219
+ }
220
+ throw new Error(`Exec not supported for path: ${path}`);
221
+ }
222
+ /**
223
+ * Lists available actions for a row
224
+ */
225
+ listActions(table, pk) {
226
+ const actionNames = this.actions.listNames({ rowLevel: true });
227
+ return {
228
+ data: buildActionsListEntry(table, pk, actionNames, { basePath: "" }),
229
+ };
230
+ }
231
+ /**
232
+ * Executes an action
233
+ */
234
+ async executeAction(table, pk, actionName, params) {
235
+ const schema = this.schemas.get(table);
236
+ if (!schema) {
237
+ throw new Error(`Table '${table}' not found`);
238
+ }
239
+ // Get row data if pk is provided
240
+ let row;
241
+ if (pk) {
242
+ const readResult = await this.crud.readRow(table, pk);
243
+ row = readResult.data?.content;
244
+ }
245
+ const ctx = {
246
+ db: this.db,
247
+ schemas: this.schemas,
248
+ table,
249
+ pk,
250
+ row,
251
+ module: {
252
+ refreshSchema: () => this.refreshSchema(),
253
+ exportTable: (t, f) => this.exportTable(t, f),
254
+ },
255
+ };
256
+ const result = await this.actions.execute(actionName, ctx, params);
257
+ if (!result.success) {
258
+ throw new Error(result.message ?? "Action failed");
259
+ }
260
+ return {
261
+ data: {
262
+ id: `${table}:${pk ?? ""}:@actions:${actionName}`,
263
+ path: pk ? `/${table}/${pk}/@actions/${actionName}` : `/${table}/@actions/${actionName}`,
264
+ content: result.data,
265
+ },
266
+ };
267
+ }
268
+ /**
269
+ * Refreshes the schema cache
270
+ */
271
+ async refreshSchema() {
272
+ const db = this.db;
273
+ const introspector = new SchemaIntrospector();
274
+ this.schemas = await introspector.introspect(db, {
275
+ tables: this.options.tables,
276
+ excludeTables: this.options.excludeTables,
277
+ });
278
+ this.crud.setSchemas(this.schemas);
279
+ this.ftsSearch.setSchemas(this.schemas);
280
+ }
281
+ /**
282
+ * Exports table data in specified format
283
+ */
284
+ async exportTable(table, format) {
285
+ const listResult = await this.crud.listTable(table, { limit: 10000 });
286
+ if (format === "csv") {
287
+ const schema = this.schemas.get(table);
288
+ if (!schema)
289
+ throw new Error(`Table '${table}' not found`);
290
+ const headers = schema.columns.map((c) => c.name).join(",");
291
+ const rows = listResult.data.map((entry) => {
292
+ const content = entry.content;
293
+ return schema.columns
294
+ .map((c) => {
295
+ const val = content[c.name];
296
+ if (val === null || val === undefined)
297
+ return "";
298
+ if (typeof val === "string" && (val.includes(",") || val.includes('"'))) {
299
+ return `"${val.replace(/"/g, '""')}"`;
300
+ }
301
+ return String(val);
302
+ })
303
+ .join(",");
304
+ });
305
+ return `${headers}\n${rows.join("\n")}`;
306
+ }
307
+ // Default: JSON
308
+ return listResult.data.map((entry) => entry.content);
309
+ }
310
+ /**
311
+ * Registers a custom action
312
+ */
313
+ registerAction(name, handler, options) {
314
+ this.actions.registerSimple(name, async (ctx, params) => ({
315
+ success: true,
316
+ data: await handler(ctx, params),
317
+ }), options);
318
+ }
319
+ /**
320
+ * Gets table schemas (for external access)
321
+ */
322
+ getSchemas() {
323
+ return this.schemas;
324
+ }
325
+ /**
326
+ * Gets the database instance (for advanced operations)
327
+ */
328
+ getDatabase() {
329
+ return this.db;
330
+ }
331
+ }
332
+ // Type check to ensure SQLiteAFS implements AFSModuleClass
333
+ const _typeCheck = SQLiteAFS;
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@aigne/afs-sqlite",
3
+ "version": "1.0.1-beta",
4
+ "description": "AIGNE AFS module for SQLite database storage with schema introspection",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
9
+ "homepage": "https://www.aigne.io/framework",
10
+ "license": "Elastic-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/AIGNE-io/aigne-framework"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/AIGNE-io/aigne-framework/issues"
17
+ },
18
+ "files": [
19
+ "lib/cjs",
20
+ "lib/dts",
21
+ "lib/esm",
22
+ "LICENSE",
23
+ "README.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "type": "module",
27
+ "main": "./lib/cjs/index.js",
28
+ "module": "./lib/esm/index.js",
29
+ "types": "./lib/dts/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./lib/esm/index.js",
33
+ "require": "./lib/cjs/index.js",
34
+ "types": "./lib/dts/index.d.ts"
35
+ },
36
+ "./*": {
37
+ "import": "./lib/esm/*",
38
+ "require": "./lib/cjs/*",
39
+ "types": "./lib/dts/*"
40
+ }
41
+ },
42
+ "typesVersions": {
43
+ "*": {
44
+ ".": [
45
+ "./lib/dts/index.d.ts"
46
+ ]
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "radix3": "^1.1.2",
51
+ "zod": "^3.25.67",
52
+ "@aigne/afs": "^1.4.0-beta.9",
53
+ "@aigne/sqlite": "^0.4.9-beta.2"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "^1.2.22",
57
+ "drizzle-orm": "^0.44.2",
58
+ "npm-run-all": "^4.1.5",
59
+ "rimraf": "^6.0.1",
60
+ "typescript": "^5.9.2",
61
+ "@aigne/test-utils": "^0.5.69-beta.23"
62
+ },
63
+ "scripts": {
64
+ "lint": "tsc --noEmit",
65
+ "build": "tsc --build scripts/tsconfig.build.json",
66
+ "clean": "rimraf lib test/coverage",
67
+ "test": "bun test",
68
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
69
+ "postbuild": "node ../../scripts/post-build-lib.mjs"
70
+ }
71
+ }