kotadb 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/package.json +75 -0
- package/src/api/auto-reindex.ts +55 -0
- package/src/api/openapi/builder.ts +209 -0
- package/src/api/openapi/paths.ts +354 -0
- package/src/api/openapi/schemas.ts +608 -0
- package/src/api/queries.ts +1168 -0
- package/src/api/routes.ts +339 -0
- package/src/auth/middleware.ts +83 -0
- package/src/cli.ts +221 -0
- package/src/config/constants.ts +96 -0
- package/src/config/environment.ts +96 -0
- package/src/config/gitignore.ts +68 -0
- package/src/config/index.ts +20 -0
- package/src/config/project-root.ts +52 -0
- package/src/db/client.ts +72 -0
- package/src/db/sqlite/index.ts +35 -0
- package/src/db/sqlite/jsonl-exporter.ts +416 -0
- package/src/db/sqlite/jsonl-importer.ts +361 -0
- package/src/db/sqlite/sqlite-client.ts +536 -0
- package/src/index.ts +66 -0
- package/src/indexer/ast-parser.ts +146 -0
- package/src/indexer/ast-types.ts +54 -0
- package/src/indexer/circular-detector.ts +262 -0
- package/src/indexer/dependency-extractor.ts +352 -0
- package/src/indexer/extractors.ts +54 -0
- package/src/indexer/import-resolver.ts +167 -0
- package/src/indexer/parsers.ts +177 -0
- package/src/indexer/reference-extractor.ts +488 -0
- package/src/indexer/repos.ts +245 -0
- package/src/indexer/storage.ts +277 -0
- package/src/indexer/symbol-extractor.ts +660 -0
- package/src/instrument.ts +88 -0
- package/src/logging/context.ts +46 -0
- package/src/logging/logger.ts +193 -0
- package/src/logging/middleware.ts +107 -0
- package/src/mcp/github-integration.ts +293 -0
- package/src/mcp/headers.ts +101 -0
- package/src/mcp/impact-analysis.ts +495 -0
- package/src/mcp/jsonrpc.ts +141 -0
- package/src/mcp/lifecycle.ts +73 -0
- package/src/mcp/server.ts +202 -0
- package/src/mcp/session.ts +44 -0
- package/src/mcp/spec-validation.ts +491 -0
- package/src/mcp/tools.ts +889 -0
- package/src/sync/deletion-manifest.ts +210 -0
- package/src/sync/index.ts +16 -0
- package/src/sync/merge-driver.ts +172 -0
- package/src/sync/watcher.ts +221 -0
- package/src/types/rate-limit.ts +88 -0
- package/src/validation/common-schemas.ts +96 -0
- package/src/validation/schemas.ts +187 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL import layer for SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* Imports JSONL (JSON Lines) files into SQLite database for:
|
|
5
|
+
* - Database recovery from backups
|
|
6
|
+
* - Sync from git repository
|
|
7
|
+
* - Migration between machines
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Transactional imports (all-or-nothing per table)
|
|
11
|
+
* - Conflict handling (INSERT OR REPLACE)
|
|
12
|
+
* - Validation of required fields
|
|
13
|
+
*
|
|
14
|
+
* @module @db/sqlite/jsonl-importer
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { applyDeletionManifest } from "@sync/deletion-manifest.js";
|
|
20
|
+
import { createLogger } from "@logging/logger.js";
|
|
21
|
+
import type { KotaDatabase } from "./sqlite-client.js";
|
|
22
|
+
|
|
23
|
+
const logger = createLogger({ module: "jsonl-importer" });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for table imports
|
|
27
|
+
*/
|
|
28
|
+
interface TableImportConfig {
|
|
29
|
+
/** Table name */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Primary key column(s) */
|
|
32
|
+
primaryKey: string | string[];
|
|
33
|
+
/** Required fields for validation */
|
|
34
|
+
requiredFields?: string[];
|
|
35
|
+
/** Use INSERT OR IGNORE instead of INSERT OR REPLACE */
|
|
36
|
+
ignoreConflicts?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default table import configurations.
|
|
41
|
+
*
|
|
42
|
+
* Local-first essentials only - code intelligence tables for indexing and search.
|
|
43
|
+
* Cloud-centric tables (users, api_keys, rate_limit_*) are excluded from defaults
|
|
44
|
+
* per the local-first pivot (#534). Custom table configuration is still supported
|
|
45
|
+
* for future sync tier functionality.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_IMPORT_CONFIGS: TableImportConfig[] = [
|
|
48
|
+
{ name: "repositories", primaryKey: "id", requiredFields: ["id", "name"] },
|
|
49
|
+
{ name: "indexed_files", primaryKey: "id", requiredFields: ["id", "repository_id", "path"] },
|
|
50
|
+
{ name: "indexed_symbols", primaryKey: "id", requiredFields: ["id", "file_id", "name"] },
|
|
51
|
+
{ name: "indexed_references", primaryKey: "id", requiredFields: ["id", "file_id", "symbol_name"] },
|
|
52
|
+
{ name: "projects", primaryKey: "id", requiredFields: ["id", "name"] },
|
|
53
|
+
{ name: "project_repositories", primaryKey: ["project_id", "repository_id"] },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Result of a single table import
|
|
58
|
+
*/
|
|
59
|
+
interface TableImportResult {
|
|
60
|
+
table: string;
|
|
61
|
+
status: "imported" | "skipped" | "error";
|
|
62
|
+
rowsImported: number;
|
|
63
|
+
rowsSkipped: number;
|
|
64
|
+
errors: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Result of a full import operation
|
|
69
|
+
*/
|
|
70
|
+
export interface ImportResult {
|
|
71
|
+
tablesImported: number;
|
|
72
|
+
tablesSkipped: number;
|
|
73
|
+
totalRowsImported: number;
|
|
74
|
+
totalRowsSkipped: number;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
results: TableImportResult[];
|
|
77
|
+
errors: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Import JSONL files from a directory into the database.
|
|
82
|
+
*
|
|
83
|
+
* @param db - Database to import into
|
|
84
|
+
* @param importDir - Directory containing JSONL files
|
|
85
|
+
* @param configs - Table configurations (optional, uses defaults)
|
|
86
|
+
* @returns Import result with statistics
|
|
87
|
+
*/
|
|
88
|
+
export async function importFromJSONL(
|
|
89
|
+
db: KotaDatabase,
|
|
90
|
+
importDir: string,
|
|
91
|
+
configs: TableImportConfig[] = DEFAULT_IMPORT_CONFIGS,
|
|
92
|
+
): Promise<ImportResult> {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
const results: TableImportResult[] = [];
|
|
95
|
+
const globalErrors: string[] = [];
|
|
96
|
+
let totalRowsImported = 0;
|
|
97
|
+
let totalRowsSkipped = 0;
|
|
98
|
+
let tablesImported = 0;
|
|
99
|
+
let tablesSkipped = 0;
|
|
100
|
+
|
|
101
|
+
logger.info("Starting JSONL import", {
|
|
102
|
+
import_dir: importDir,
|
|
103
|
+
tables: configs.map((c) => c.name),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Apply deletion manifest first
|
|
107
|
+
const deletionManifestPath = join(importDir, ".deletions.jsonl");
|
|
108
|
+
if (existsSync(deletionManifestPath)) {
|
|
109
|
+
const deletionResult = await applyDeletionManifest(db, deletionManifestPath);
|
|
110
|
+
logger.info("Deletion manifest applied", {
|
|
111
|
+
deleted_count: deletionResult.deletedCount,
|
|
112
|
+
errors: deletionResult.errors
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
// Validate import directory exists
|
|
118
|
+
if (!existsSync(importDir)) {
|
|
119
|
+
const error = "Import directory not found: " + importDir;
|
|
120
|
+
logger.error(error);
|
|
121
|
+
return {
|
|
122
|
+
tablesImported: 0,
|
|
123
|
+
tablesSkipped: configs.length,
|
|
124
|
+
totalRowsImported: 0,
|
|
125
|
+
totalRowsSkipped: 0,
|
|
126
|
+
durationMs: Date.now() - startTime,
|
|
127
|
+
results: [],
|
|
128
|
+
errors: [error],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Import tables in order (respecting foreign key constraints)
|
|
133
|
+
for (const config of configs) {
|
|
134
|
+
try {
|
|
135
|
+
const result = await importTable(db, importDir, config);
|
|
136
|
+
results.push(result);
|
|
137
|
+
|
|
138
|
+
if (result.status === "imported") {
|
|
139
|
+
tablesImported++;
|
|
140
|
+
totalRowsImported += result.rowsImported;
|
|
141
|
+
totalRowsSkipped += result.rowsSkipped;
|
|
142
|
+
} else {
|
|
143
|
+
tablesSkipped++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (result.errors.length > 0) {
|
|
147
|
+
globalErrors.push(...result.errors.map((e) => config.name + ": " + e));
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
151
|
+
logger.error("Failed to import table", new Error(errorMessage), {
|
|
152
|
+
table: config.name,
|
|
153
|
+
});
|
|
154
|
+
results.push({
|
|
155
|
+
table: config.name,
|
|
156
|
+
status: "error",
|
|
157
|
+
rowsImported: 0,
|
|
158
|
+
rowsSkipped: 0,
|
|
159
|
+
errors: [errorMessage],
|
|
160
|
+
});
|
|
161
|
+
globalErrors.push(config.name + ": " + errorMessage);
|
|
162
|
+
tablesSkipped++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const duration = Date.now() - startTime;
|
|
167
|
+
|
|
168
|
+
logger.info("JSONL import completed", {
|
|
169
|
+
tables_imported: tablesImported,
|
|
170
|
+
tables_skipped: tablesSkipped,
|
|
171
|
+
total_rows_imported: totalRowsImported,
|
|
172
|
+
total_rows_skipped: totalRowsSkipped,
|
|
173
|
+
duration_ms: duration,
|
|
174
|
+
error_count: globalErrors.length,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
tablesImported,
|
|
179
|
+
tablesSkipped,
|
|
180
|
+
totalRowsImported,
|
|
181
|
+
totalRowsSkipped,
|
|
182
|
+
durationMs: duration,
|
|
183
|
+
results,
|
|
184
|
+
errors: globalErrors,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Import a single JSONL file into a table
|
|
190
|
+
*/
|
|
191
|
+
async function importTable(
|
|
192
|
+
db: KotaDatabase,
|
|
193
|
+
importDir: string,
|
|
194
|
+
config: TableImportConfig,
|
|
195
|
+
): Promise<TableImportResult> {
|
|
196
|
+
const { name, requiredFields = [], ignoreConflicts = false } = config;
|
|
197
|
+
const filepath = join(importDir, name + ".jsonl");
|
|
198
|
+
|
|
199
|
+
// Check if file exists
|
|
200
|
+
if (!existsSync(filepath)) {
|
|
201
|
+
logger.debug("JSONL file not found, skipping", { table: name, file: filepath });
|
|
202
|
+
return {
|
|
203
|
+
table: name,
|
|
204
|
+
status: "skipped",
|
|
205
|
+
rowsImported: 0,
|
|
206
|
+
rowsSkipped: 0,
|
|
207
|
+
errors: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Read and parse file
|
|
212
|
+
const content = await Bun.file(filepath).text();
|
|
213
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
214
|
+
|
|
215
|
+
if (lines.length === 0) {
|
|
216
|
+
logger.debug("Empty JSONL file, skipping", { table: name });
|
|
217
|
+
return {
|
|
218
|
+
table: name,
|
|
219
|
+
status: "skipped",
|
|
220
|
+
rowsImported: 0,
|
|
221
|
+
rowsSkipped: 0,
|
|
222
|
+
errors: [],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const errors: string[] = [];
|
|
227
|
+
let rowsImported = 0;
|
|
228
|
+
let rowsSkipped = 0;
|
|
229
|
+
|
|
230
|
+
// Import within a transaction
|
|
231
|
+
try {
|
|
232
|
+
db.immediateTransaction(() => {
|
|
233
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
234
|
+
const line = lines[idx];
|
|
235
|
+
if (!line) continue;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
239
|
+
|
|
240
|
+
// Validate required fields
|
|
241
|
+
const missingFields = requiredFields.filter((field) => !(field in row));
|
|
242
|
+
if (missingFields.length > 0) {
|
|
243
|
+
errors.push("Row " + (idx + 1) + ": Missing required fields: " + missingFields.join(", "));
|
|
244
|
+
rowsSkipped++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Build INSERT statement
|
|
249
|
+
const columns = Object.keys(row);
|
|
250
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
251
|
+
const values = columns.map((col) => {
|
|
252
|
+
const value = row[col];
|
|
253
|
+
// Convert objects/arrays to JSON strings
|
|
254
|
+
if (typeof value === "object" && value !== null) {
|
|
255
|
+
return JSON.stringify(value);
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const conflictClause = ignoreConflicts ? "OR IGNORE" : "OR REPLACE";
|
|
261
|
+
const sql = "INSERT " + conflictClause + " INTO " + name + " (" + columns.join(", ") + ") VALUES (" + placeholders + ")";
|
|
262
|
+
|
|
263
|
+
db.run(sql, values as (string | number | bigint | boolean | null | Uint8Array)[]);
|
|
264
|
+
rowsImported++;
|
|
265
|
+
} catch (parseError) {
|
|
266
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
267
|
+
errors.push("Row " + (idx + 1) + ": " + errorMessage);
|
|
268
|
+
rowsSkipped++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
} catch (txError) {
|
|
273
|
+
const errorMessage = txError instanceof Error ? txError.message : String(txError);
|
|
274
|
+
logger.error("Transaction failed for table import", new Error(errorMessage), {
|
|
275
|
+
table: name,
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
table: name,
|
|
279
|
+
status: "error",
|
|
280
|
+
rowsImported: 0,
|
|
281
|
+
rowsSkipped: lines.length,
|
|
282
|
+
errors: [errorMessage],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
logger.info("Imported table", {
|
|
287
|
+
table: name,
|
|
288
|
+
rows_imported: rowsImported,
|
|
289
|
+
rows_skipped: rowsSkipped,
|
|
290
|
+
error_count: errors.length,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
table: name,
|
|
295
|
+
status: "imported",
|
|
296
|
+
rowsImported,
|
|
297
|
+
rowsSkipped,
|
|
298
|
+
errors,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Import a specific table from JSONL
|
|
304
|
+
*/
|
|
305
|
+
export async function importTableFromJSONL(
|
|
306
|
+
db: KotaDatabase,
|
|
307
|
+
filepath: string,
|
|
308
|
+
tableName: string,
|
|
309
|
+
options: Partial<TableImportConfig> = {},
|
|
310
|
+
): Promise<TableImportResult> {
|
|
311
|
+
const config: TableImportConfig = {
|
|
312
|
+
name: tableName,
|
|
313
|
+
primaryKey: options.primaryKey || "id",
|
|
314
|
+
requiredFields: options.requiredFields,
|
|
315
|
+
ignoreConflicts: options.ignoreConflicts,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Extract directory from filepath
|
|
319
|
+
const importDir = filepath.replace("/" + tableName + ".jsonl", "").replace("\\" + tableName + ".jsonl", "");
|
|
320
|
+
|
|
321
|
+
return importTable(db, importDir, config);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Validate a JSONL file without importing
|
|
326
|
+
*/
|
|
327
|
+
export async function validateJSONL(filepath: string): Promise<{
|
|
328
|
+
valid: boolean;
|
|
329
|
+
lineCount: number;
|
|
330
|
+
errors: string[];
|
|
331
|
+
}> {
|
|
332
|
+
if (!existsSync(filepath)) {
|
|
333
|
+
return {
|
|
334
|
+
valid: false,
|
|
335
|
+
lineCount: 0,
|
|
336
|
+
errors: ["File not found"],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const content = await Bun.file(filepath).text();
|
|
341
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
342
|
+
const errors: string[] = [];
|
|
343
|
+
|
|
344
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
345
|
+
const line = lines[idx];
|
|
346
|
+
if (!line) continue;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
JSON.parse(line);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
352
|
+
errors.push("Line " + (idx + 1) + ": Invalid JSON - " + message);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
valid: errors.length === 0,
|
|
358
|
+
lineCount: lines.length,
|
|
359
|
+
errors,
|
|
360
|
+
};
|
|
361
|
+
}
|