@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,221 @@
1
+ import { sql } from "@aigne/sqlite";
2
+ import { buildAttributeEntry, buildAttributeListEntry, buildMetaEntry, buildRowEntry, buildSchemaEntry, buildTableEntry, } from "../node/builder.js";
3
+ import { buildDelete, buildGetLastRowId, buildInsert, buildSelectAll, buildSelectByPK, buildUpdate, } from "./query-builder.js";
4
+ /**
5
+ * Executes a raw SQL query and returns all rows
6
+ */
7
+ async function execAll(db, query) {
8
+ return db.all(sql.raw(query)).execute();
9
+ }
10
+ /**
11
+ * Executes a raw SQL query (for INSERT, UPDATE, DELETE)
12
+ */
13
+ async function execRun(db, query) {
14
+ await db.run(sql.raw(query)).execute();
15
+ }
16
+ /**
17
+ * CRUD operations for SQLite AFS
18
+ */
19
+ export class CRUDOperations {
20
+ db;
21
+ schemas;
22
+ basePath;
23
+ constructor(db, schemas, basePath = "") {
24
+ this.db = db;
25
+ this.schemas = schemas;
26
+ this.basePath = basePath;
27
+ }
28
+ /**
29
+ * Lists all tables
30
+ */
31
+ async listTables() {
32
+ const entries = [];
33
+ const buildOptions = { basePath: this.basePath };
34
+ for (const [name, schema] of this.schemas) {
35
+ // Get row count for each table
36
+ const countResult = await execAll(this.db, `SELECT COUNT(*) as count FROM "${name}"`);
37
+ const rowCount = countResult[0]?.count ?? 0;
38
+ entries.push(buildTableEntry(name, schema, { ...buildOptions, rowCount }));
39
+ }
40
+ return { data: entries };
41
+ }
42
+ /**
43
+ * Lists rows in a table
44
+ */
45
+ async listTable(table, options) {
46
+ const schema = this.schemas.get(table);
47
+ if (!schema) {
48
+ return { data: [], message: `Table '${table}' not found` };
49
+ }
50
+ const buildOptions = { basePath: this.basePath };
51
+ const queryStr = buildSelectAll(table, {
52
+ limit: options?.limit ?? 100,
53
+ orderBy: options?.orderBy,
54
+ });
55
+ const rows = await execAll(this.db, queryStr);
56
+ const entries = rows.map((row) => buildRowEntry(table, schema, row, buildOptions));
57
+ return { data: entries };
58
+ }
59
+ /**
60
+ * Reads a single row by primary key
61
+ */
62
+ async readRow(table, pk) {
63
+ const schema = this.schemas.get(table);
64
+ if (!schema) {
65
+ return { message: `Table '${table}' not found` };
66
+ }
67
+ const buildOptions = { basePath: this.basePath };
68
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
69
+ const row = rows[0];
70
+ if (!row) {
71
+ return { message: `Row with pk '${pk}' not found in table '${table}'` };
72
+ }
73
+ return { data: buildRowEntry(table, schema, row, buildOptions) };
74
+ }
75
+ /**
76
+ * Gets table schema
77
+ */
78
+ getSchema(table) {
79
+ const schema = this.schemas.get(table);
80
+ if (!schema) {
81
+ return { message: `Table '${table}' not found` };
82
+ }
83
+ const buildOptions = { basePath: this.basePath };
84
+ return { data: buildSchemaEntry(table, schema, buildOptions) };
85
+ }
86
+ /**
87
+ * Lists attributes (columns) for a row
88
+ */
89
+ async listAttributes(table, pk) {
90
+ const schema = this.schemas.get(table);
91
+ if (!schema) {
92
+ return { data: [], message: `Table '${table}' not found` };
93
+ }
94
+ const buildOptions = { basePath: this.basePath };
95
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
96
+ const row = rows[0];
97
+ if (!row) {
98
+ return { data: [], message: `Row with pk '${pk}' not found` };
99
+ }
100
+ return { data: buildAttributeListEntry(table, schema, pk, row, buildOptions) };
101
+ }
102
+ /**
103
+ * Gets a single attribute (column value) for a row
104
+ */
105
+ async getAttribute(table, pk, column) {
106
+ const schema = this.schemas.get(table);
107
+ if (!schema) {
108
+ return { message: `Table '${table}' not found` };
109
+ }
110
+ // Validate column exists
111
+ const colInfo = schema.columns.find((c) => c.name === column);
112
+ if (!colInfo) {
113
+ return { message: `Column '${column}' not found in table '${table}'` };
114
+ }
115
+ const buildOptions = { basePath: this.basePath };
116
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
117
+ if (rows.length === 0) {
118
+ return { message: `Row with pk '${pk}' not found` };
119
+ }
120
+ return {
121
+ data: buildAttributeEntry(table, pk, column, rows[0]?.[column], buildOptions),
122
+ };
123
+ }
124
+ /**
125
+ * Gets row metadata
126
+ */
127
+ async getMeta(table, pk) {
128
+ const schema = this.schemas.get(table);
129
+ if (!schema) {
130
+ return { message: `Table '${table}' not found` };
131
+ }
132
+ const buildOptions = { basePath: this.basePath };
133
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
134
+ const row = rows[0];
135
+ if (!row) {
136
+ return { message: `Row with pk '${pk}' not found` };
137
+ }
138
+ return { data: buildMetaEntry(table, schema, pk, row, buildOptions) };
139
+ }
140
+ /**
141
+ * Creates a new row in a table
142
+ */
143
+ async createRow(table, content) {
144
+ const schema = this.schemas.get(table);
145
+ if (!schema) {
146
+ throw new Error(`Table '${table}' not found`);
147
+ }
148
+ const buildOptions = { basePath: this.basePath };
149
+ // Insert the row
150
+ await execRun(this.db, buildInsert(table, schema, content));
151
+ // Get the last inserted rowid
152
+ const lastIdResult = await execAll(this.db, buildGetLastRowId());
153
+ const lastId = lastIdResult[0]?.id;
154
+ if (lastId === undefined) {
155
+ throw new Error("Failed to get last inserted row ID");
156
+ }
157
+ // Fetch the inserted row
158
+ const pkColumn = schema.primaryKey[0] ?? "rowid";
159
+ const pk = content[pkColumn] !== undefined ? String(content[pkColumn]) : String(lastId);
160
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
161
+ const row = rows[0];
162
+ if (!row) {
163
+ throw new Error("Failed to fetch inserted row");
164
+ }
165
+ return { data: buildRowEntry(table, schema, row, buildOptions) };
166
+ }
167
+ /**
168
+ * Updates an existing row
169
+ */
170
+ async updateRow(table, pk, content) {
171
+ const schema = this.schemas.get(table);
172
+ if (!schema) {
173
+ throw new Error(`Table '${table}' not found`);
174
+ }
175
+ const buildOptions = { basePath: this.basePath };
176
+ // Update the row
177
+ await execRun(this.db, buildUpdate(table, schema, pk, content));
178
+ // Fetch the updated row
179
+ const rows = await execAll(this.db, buildSelectByPK(table, schema, pk));
180
+ const row = rows[0];
181
+ if (!row) {
182
+ throw new Error(`Row with pk '${pk}' not found after update`);
183
+ }
184
+ return { data: buildRowEntry(table, schema, row, buildOptions) };
185
+ }
186
+ /**
187
+ * Deletes a row by primary key
188
+ */
189
+ async deleteRow(table, pk) {
190
+ const schema = this.schemas.get(table);
191
+ if (!schema) {
192
+ throw new Error(`Table '${table}' not found`);
193
+ }
194
+ // Check if row exists first
195
+ const existing = await execAll(this.db, buildSelectByPK(table, schema, pk));
196
+ if (existing.length === 0) {
197
+ return { message: `Row with pk '${pk}' not found in table '${table}'` };
198
+ }
199
+ // Delete the row
200
+ await execRun(this.db, buildDelete(table, schema, pk));
201
+ return { message: `Deleted row '${pk}' from table '${table}'` };
202
+ }
203
+ /**
204
+ * Checks if a table exists
205
+ */
206
+ hasTable(table) {
207
+ return this.schemas.has(table);
208
+ }
209
+ /**
210
+ * Gets the schema for a table
211
+ */
212
+ getTableSchema(table) {
213
+ return this.schemas.get(table);
214
+ }
215
+ /**
216
+ * Updates the schemas map (after refresh)
217
+ */
218
+ setSchemas(schemas) {
219
+ this.schemas = schemas;
220
+ }
221
+ }
@@ -0,0 +1,37 @@
1
+ import type { TableSchema } from "../schema/types.js";
2
+ /**
3
+ * Builds a SELECT query string for a single row by primary key
4
+ */
5
+ export declare function buildSelectByPK(tableName: string, schema: TableSchema, pk: string): string;
6
+ /**
7
+ * Builds a SELECT query string for listing rows with optional limit and offset
8
+ */
9
+ export declare function buildSelectAll(tableName: string, options?: {
10
+ limit?: number;
11
+ offset?: number;
12
+ orderBy?: [string, "asc" | "desc"][];
13
+ }): string;
14
+ /**
15
+ * Builds an INSERT query string from content object
16
+ */
17
+ export declare function buildInsert(tableName: string, schema: TableSchema, content: Record<string, unknown>): string;
18
+ /**
19
+ * Builds an UPDATE query string from content object
20
+ */
21
+ export declare function buildUpdate(tableName: string, schema: TableSchema, pk: string, content: Record<string, unknown>): string;
22
+ /**
23
+ * Builds a DELETE query string by primary key
24
+ */
25
+ export declare function buildDelete(tableName: string, schema: TableSchema, pk: string): string;
26
+ /**
27
+ * Formats a value for SQL insertion
28
+ */
29
+ export declare function formatValue(value: unknown): string;
30
+ /**
31
+ * Escapes a string for safe SQL insertion
32
+ */
33
+ export declare function escapeSQLString(str: string): string;
34
+ /**
35
+ * Gets the last inserted rowid query string
36
+ */
37
+ export declare function buildGetLastRowId(): string;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Builds a SELECT query string for a single row by primary key
3
+ */
4
+ export function buildSelectByPK(tableName, schema, pk) {
5
+ const pkColumn = schema.primaryKey[0] ?? "rowid";
6
+ return `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = '${escapeSQLString(pk)}'`;
7
+ }
8
+ /**
9
+ * Builds a SELECT query string for listing rows with optional limit and offset
10
+ */
11
+ export function buildSelectAll(tableName, options) {
12
+ let query = `SELECT * FROM "${tableName}"`;
13
+ if (options?.orderBy?.length) {
14
+ const orderClauses = options.orderBy.map(([col, dir]) => `"${col}" ${dir.toUpperCase()}`);
15
+ query += ` ORDER BY ${orderClauses.join(", ")}`;
16
+ }
17
+ if (options?.limit !== undefined) {
18
+ query += ` LIMIT ${options.limit}`;
19
+ }
20
+ if (options?.offset !== undefined) {
21
+ query += ` OFFSET ${options.offset}`;
22
+ }
23
+ return query;
24
+ }
25
+ /**
26
+ * Builds an INSERT query string from content object
27
+ */
28
+ export function buildInsert(tableName, schema, content) {
29
+ // Filter to only valid columns
30
+ const validColumns = new Set(schema.columns.map((c) => c.name));
31
+ const entries = Object.entries(content).filter(([key]) => validColumns.has(key));
32
+ if (entries.length === 0) {
33
+ throw new Error(`No valid columns provided for INSERT into ${tableName}`);
34
+ }
35
+ const columns = entries.map(([key]) => `"${key}"`).join(", ");
36
+ const values = entries.map(([, value]) => formatValue(value)).join(", ");
37
+ return `INSERT INTO "${tableName}" (${columns}) VALUES (${values})`;
38
+ }
39
+ /**
40
+ * Builds an UPDATE query string from content object
41
+ */
42
+ export function buildUpdate(tableName, schema, pk, content) {
43
+ const pkColumn = schema.primaryKey[0] ?? "rowid";
44
+ // Filter to only valid columns, excluding PK
45
+ const validColumns = new Set(schema.columns.map((c) => c.name));
46
+ const entries = Object.entries(content).filter(([key]) => validColumns.has(key) && key !== pkColumn);
47
+ if (entries.length === 0) {
48
+ throw new Error(`No valid columns provided for UPDATE on ${tableName}`);
49
+ }
50
+ const setClauses = entries.map(([key, value]) => `"${key}" = ${formatValue(value)}`).join(", ");
51
+ return `UPDATE "${tableName}" SET ${setClauses} WHERE "${pkColumn}" = '${escapeSQLString(pk)}'`;
52
+ }
53
+ /**
54
+ * Builds a DELETE query string by primary key
55
+ */
56
+ export function buildDelete(tableName, schema, pk) {
57
+ const pkColumn = schema.primaryKey[0] ?? "rowid";
58
+ return `DELETE FROM "${tableName}" WHERE "${pkColumn}" = '${escapeSQLString(pk)}'`;
59
+ }
60
+ /**
61
+ * Formats a value for SQL insertion
62
+ */
63
+ export function formatValue(value) {
64
+ if (value === null || value === undefined) {
65
+ return "NULL";
66
+ }
67
+ if (typeof value === "number") {
68
+ return String(value);
69
+ }
70
+ if (typeof value === "boolean") {
71
+ return value ? "1" : "0";
72
+ }
73
+ if (value instanceof Date) {
74
+ return `'${value.toISOString()}'`;
75
+ }
76
+ if (typeof value === "object") {
77
+ return `'${escapeSQLString(JSON.stringify(value))}'`;
78
+ }
79
+ return `'${escapeSQLString(String(value))}'`;
80
+ }
81
+ /**
82
+ * Escapes a string for safe SQL insertion
83
+ */
84
+ export function escapeSQLString(str) {
85
+ return str.replace(/'/g, "''");
86
+ }
87
+ /**
88
+ * Gets the last inserted rowid query string
89
+ */
90
+ export function buildGetLastRowId() {
91
+ return "SELECT last_insert_rowid() as id";
92
+ }
@@ -0,0 +1,75 @@
1
+ import type { AFSSearchOptions, AFSSearchResult } from "@aigne/afs";
2
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
3
+ import type { TableSchema } from "../schema/types.js";
4
+ /**
5
+ * FTS5 search configuration for a table
6
+ */
7
+ export interface FTSTableConfig {
8
+ /** Columns to include in FTS index */
9
+ columns: string[];
10
+ /** Whether FTS table has been created */
11
+ initialized?: boolean;
12
+ }
13
+ /**
14
+ * FTS5 search configuration
15
+ */
16
+ export interface FTSConfig {
17
+ /** Whether FTS is enabled */
18
+ enabled: boolean;
19
+ /** Per-table FTS configuration */
20
+ tables: Map<string, FTSTableConfig>;
21
+ }
22
+ /**
23
+ * FTS5 Search operations for SQLite AFS
24
+ */
25
+ export declare class FTSSearch {
26
+ private db;
27
+ private schemas;
28
+ private config;
29
+ private basePath;
30
+ constructor(db: LibSQLDatabase, schemas: Map<string, TableSchema>, config: FTSConfig, basePath?: string);
31
+ /**
32
+ * Performs full-text search across configured tables
33
+ */
34
+ search(query: string, options?: AFSSearchOptions & {
35
+ /** Specific tables to search (defaults to all FTS-enabled tables) */
36
+ tables?: string[];
37
+ }): Promise<AFSSearchResult>;
38
+ /**
39
+ * Searches within a specific table
40
+ */
41
+ searchTable(tableName: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
42
+ /**
43
+ * Checks if FTS is configured for a table
44
+ */
45
+ hasFTS(tableName: string): boolean;
46
+ /**
47
+ * Gets FTS configuration for a table
48
+ */
49
+ getFTSConfig(tableName: string): FTSTableConfig | undefined;
50
+ /**
51
+ * Checks if an FTS table exists
52
+ */
53
+ private ftsTableExists;
54
+ /**
55
+ * Prepares a query string for FTS5
56
+ * Handles special characters and case sensitivity
57
+ */
58
+ private prepareFTSQuery;
59
+ /**
60
+ * Updates the schemas map (after refresh)
61
+ */
62
+ setSchemas(schemas: Map<string, TableSchema>): void;
63
+ /**
64
+ * Simple search fallback when FTS is not available
65
+ * Uses LIKE queries on specified columns
66
+ */
67
+ simpleLikeSearch(tableName: string, query: string, columns: string[], options?: AFSSearchOptions): Promise<AFSSearchResult>;
68
+ }
69
+ /**
70
+ * Creates FTS configuration from options
71
+ */
72
+ export declare function createFTSConfig(options?: {
73
+ enabled?: boolean;
74
+ tables?: Record<string, string[]>;
75
+ }): FTSConfig;
@@ -0,0 +1,167 @@
1
+ import { sql } from "@aigne/sqlite";
2
+ import { buildSearchEntry } from "../node/builder.js";
3
+ /**
4
+ * Executes a raw SQL query and returns all rows
5
+ */
6
+ async function execAll(db, query) {
7
+ return db.all(sql.raw(query)).execute();
8
+ }
9
+ /**
10
+ * FTS5 Search operations for SQLite AFS
11
+ */
12
+ export class FTSSearch {
13
+ db;
14
+ schemas;
15
+ config;
16
+ basePath;
17
+ constructor(db, schemas, config, basePath = "") {
18
+ this.db = db;
19
+ this.schemas = schemas;
20
+ this.config = config;
21
+ this.basePath = basePath;
22
+ }
23
+ /**
24
+ * Performs full-text search across configured tables
25
+ */
26
+ async search(query, options) {
27
+ if (!this.config.enabled) {
28
+ return { data: [], message: "Full-text search is not enabled" };
29
+ }
30
+ const results = [];
31
+ const limit = options?.limit ?? 50;
32
+ const buildOptions = { basePath: this.basePath };
33
+ // Determine which tables to search
34
+ const tablesToSearch = options?.tables
35
+ ? options.tables.filter((t) => this.config.tables.has(t))
36
+ : Array.from(this.config.tables.keys());
37
+ // Escape and prepare the query for FTS5
38
+ const ftsQuery = this.prepareFTSQuery(query, options?.caseSensitive);
39
+ for (const tableName of tablesToSearch) {
40
+ const tableConfig = this.config.tables.get(tableName);
41
+ const schema = this.schemas.get(tableName);
42
+ if (!tableConfig || !schema)
43
+ continue;
44
+ const ftsTableName = `${tableName}_fts`;
45
+ try {
46
+ // Check if FTS table exists
47
+ const ftsExists = await this.ftsTableExists(ftsTableName);
48
+ if (!ftsExists)
49
+ continue;
50
+ // Get the first column for highlighting
51
+ const highlightColumn = tableConfig.columns[0] ?? "";
52
+ const highlightIndex = highlightColumn ? tableConfig.columns.indexOf(highlightColumn) : 0;
53
+ // Build FTS query with highlight
54
+ const rows = await execAll(this.db, `
55
+ SELECT t.*, highlight("${ftsTableName}", ${highlightIndex}, '<mark>', '</mark>') as snippet
56
+ FROM "${ftsTableName}" fts
57
+ JOIN "${tableName}" t ON fts.rowid = t.rowid
58
+ WHERE "${ftsTableName}" MATCH '${ftsQuery}'
59
+ LIMIT ${Math.ceil(limit / tablesToSearch.length)}
60
+ `);
61
+ for (const row of rows) {
62
+ const { snippet, ...rowData } = row;
63
+ results.push(buildSearchEntry(tableName, schema, rowData, snippet, buildOptions));
64
+ }
65
+ }
66
+ catch (error) {
67
+ // Log but continue with other tables
68
+ console.warn(`FTS search failed for table ${tableName}:`, error);
69
+ }
70
+ // Stop if we have enough results
71
+ if (results.length >= limit)
72
+ break;
73
+ }
74
+ return {
75
+ data: results.slice(0, limit),
76
+ message: results.length === 0 ? `No results found for "${query}"` : undefined,
77
+ };
78
+ }
79
+ /**
80
+ * Searches within a specific table
81
+ */
82
+ async searchTable(tableName, query, options) {
83
+ return this.search(query, { ...options, tables: [tableName] });
84
+ }
85
+ /**
86
+ * Checks if FTS is configured for a table
87
+ */
88
+ hasFTS(tableName) {
89
+ return this.config.enabled && this.config.tables.has(tableName);
90
+ }
91
+ /**
92
+ * Gets FTS configuration for a table
93
+ */
94
+ getFTSConfig(tableName) {
95
+ return this.config.tables.get(tableName);
96
+ }
97
+ /**
98
+ * Checks if an FTS table exists
99
+ */
100
+ async ftsTableExists(ftsTableName) {
101
+ const result = await execAll(this.db, `SELECT name FROM sqlite_master WHERE type = 'table' AND name = '${ftsTableName}'`);
102
+ return result.length > 0;
103
+ }
104
+ /**
105
+ * Prepares a query string for FTS5
106
+ * Handles special characters and case sensitivity
107
+ */
108
+ prepareFTSQuery(query, _caseSensitive) {
109
+ // Escape special FTS5 characters
110
+ let prepared = query
111
+ .replace(/"/g, '""') // Escape double quotes
112
+ .replace(/'/g, "''"); // Escape single quotes
113
+ // For case-insensitive search (default), we don't need to modify
114
+ // FTS5 is case-insensitive by default for ASCII
115
+ // If the query contains multiple words, search for the phrase
116
+ if (prepared.includes(" ") && !prepared.startsWith('"')) {
117
+ prepared = `"${prepared}"`;
118
+ }
119
+ return prepared;
120
+ }
121
+ /**
122
+ * Updates the schemas map (after refresh)
123
+ */
124
+ setSchemas(schemas) {
125
+ this.schemas = schemas;
126
+ }
127
+ /**
128
+ * Simple search fallback when FTS is not available
129
+ * Uses LIKE queries on specified columns
130
+ */
131
+ async simpleLikeSearch(tableName, query, columns, options) {
132
+ const schema = this.schemas.get(tableName);
133
+ if (!schema) {
134
+ return { data: [], message: `Table '${tableName}' not found` };
135
+ }
136
+ const buildOptions = { basePath: this.basePath };
137
+ const limit = options?.limit ?? 50;
138
+ const escapedQuery = query.replace(/'/g, "''");
139
+ // Build LIKE conditions for each column
140
+ const conditions = columns
141
+ .filter((col) => schema.columns.some((c) => c.name === col))
142
+ .map((col) => `"${col}" LIKE '%${escapedQuery}%'`)
143
+ .join(" OR ");
144
+ if (!conditions) {
145
+ return { data: [], message: "No valid columns to search" };
146
+ }
147
+ const rows = await execAll(this.db, `SELECT * FROM "${tableName}" WHERE ${conditions} LIMIT ${limit}`);
148
+ return {
149
+ data: rows.map((row) => buildSearchEntry(tableName, schema, row, undefined, buildOptions)),
150
+ };
151
+ }
152
+ }
153
+ /**
154
+ * Creates FTS configuration from options
155
+ */
156
+ export function createFTSConfig(options) {
157
+ const config = {
158
+ enabled: options?.enabled ?? false,
159
+ tables: new Map(),
160
+ };
161
+ if (options?.tables) {
162
+ for (const [table, columns] of Object.entries(options.tables)) {
163
+ config.tables.set(table, { columns });
164
+ }
165
+ }
166
+ return config;
167
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,38 @@
1
+ import { type RadixRouter } from "radix3";
2
+ import type { RouteData, RouteMatch } from "./types.js";
3
+ export type { RouteData };
4
+ /**
5
+ * Creates a radix3 router for SQLite AFS path routing
6
+ *
7
+ * Routes:
8
+ * - / → listTables
9
+ * - /:table → listTable
10
+ * - /:table/new → createRow
11
+ * - /:table/@schema → getSchema
12
+ * - /:table/:pk → readRow
13
+ * - /:table/:pk/@attr → listAttributes
14
+ * - /:table/:pk/@attr/:column → getAttribute
15
+ * - /:table/:pk/@meta → getMeta
16
+ * - /:table/:pk/@actions → listActions
17
+ * - /:table/:pk/@actions/:action → executeAction
18
+ */
19
+ export declare function createPathRouter(): RadixRouter<RouteData>;
20
+ /**
21
+ * Parses a path and returns the matched route with params
22
+ * @param router - The radix3 router instance
23
+ * @param path - The path to match
24
+ * @returns RouteMatch if matched, undefined otherwise
25
+ */
26
+ export declare function matchPath(router: RadixRouter<RouteData>, path: string): RouteMatch | undefined;
27
+ /**
28
+ * Builds a path from components
29
+ */
30
+ export declare function buildPath(table?: string, pk?: string, suffix?: string): string;
31
+ /**
32
+ * Checks if a path segment is a virtual path (@attr, @meta, @actions, @schema)
33
+ */
34
+ export declare function isVirtualPath(segment: string): boolean;
35
+ /**
36
+ * Gets the type of virtual path
37
+ */
38
+ export declare function getVirtualPathType(segment: string): "attr" | "meta" | "actions" | "schema" | null;