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