aiex-cli 0.0.6 → 0.0.7-beta.2
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 +1 -0
- package/dist/cli.mjs +14058 -14044
- package/dist/{completions-Bh0DOngr.mjs → completions-DPALvX54.mjs} +1 -1
- package/dist/core/schema-sqlite/migrate-helper.mjs +1 -1
- package/dist/{doctor-collector-SgMb2oQN.mjs → generate-drizzle-schema-D_oDmf4J.mjs} +717 -639
- package/dist/index.d.mts +78 -58
- package/dist/index.mjs +1 -1
- package/dist/table-schema.json +18 -0
- package/dist/web/assets/{JsonSchemaEditor-CfPzcMKJ.js → JsonSchemaEditor-ITVm2zG1.js} +1 -1
- package/dist/web/assets/{index-D7eI2nAX.js → index-BL42R5MF.js} +3 -3
- package/dist/web/assets/object-utils-CqCiBqJ4.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/core/schema-sqlite/migration-name.ts +1 -14
- package/dist/web/assets/object-utils-C6FkG7fw.js +0 -1
|
@@ -9,83 +9,9 @@ import { readFile, writeFile } from "jsonfile";
|
|
|
9
9
|
import Conf from "conf";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
-
//#region src/core/doctor.ts
|
|
13
|
-
function buildDoctorDiagnostics(input) {
|
|
14
|
-
return {
|
|
15
|
-
cli: {
|
|
16
|
-
name: input.pkg.name,
|
|
17
|
-
version: input.pkg.version,
|
|
18
|
-
executable: input.executable
|
|
19
|
-
},
|
|
20
|
-
runtime: {
|
|
21
|
-
node: input.node,
|
|
22
|
-
platform: input.platform,
|
|
23
|
-
arch: input.arch,
|
|
24
|
-
shell: input.shell,
|
|
25
|
-
packageManager: input.packageManager
|
|
26
|
-
},
|
|
27
|
-
system: {
|
|
28
|
-
os: `${input.osType} ${input.osRelease}`,
|
|
29
|
-
cwd: input.cwd
|
|
30
|
-
},
|
|
31
|
-
imageOcr: { ...input.imageOcr },
|
|
32
|
-
config: {
|
|
33
|
-
path: input.configPath,
|
|
34
|
-
keys: [...input.configStoreKeys].sort()
|
|
35
|
-
},
|
|
36
|
-
project: { ...input.project }
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function formatDoctorDiagnosticsJson(d) {
|
|
40
|
-
return `${JSON.stringify(d, null, 2)}\n`;
|
|
41
|
-
}
|
|
42
|
-
function doctorDiagnosticsTableRows(d) {
|
|
43
|
-
const rows = [
|
|
44
|
-
["node", d.runtime.node],
|
|
45
|
-
["platform", d.runtime.platform],
|
|
46
|
-
["arch", d.runtime.arch],
|
|
47
|
-
["shell", d.runtime.shell],
|
|
48
|
-
["packageManager", d.runtime.packageManager],
|
|
49
|
-
["os", d.system.os],
|
|
50
|
-
["cwd", d.system.cwd],
|
|
51
|
-
["config", d.config.path],
|
|
52
|
-
["configKeys", d.config.keys.join(", ")]
|
|
53
|
-
];
|
|
54
|
-
const p = d.project;
|
|
55
|
-
rows.push(["aiexDir", p.aiexDir]);
|
|
56
|
-
rows.push(["dirExists", String(p.dirExists)]);
|
|
57
|
-
rows.push(["schemaFiles", `${p.schemaCount} (${p.schemaFiles.join(", ") || "none"})`]);
|
|
58
|
-
rows.push(["aiConfig", String(p.aiConfig)]);
|
|
59
|
-
rows.push(["aiApiKeySet", String(p.aiApiKeySet)]);
|
|
60
|
-
rows.push(["aiModels", p.aiModelCount ? p.aiModels.join(", ") : "none"]);
|
|
61
|
-
rows.push(["aiVisionModels", String(p.aiVisionModelCount)]);
|
|
62
|
-
rows.push(["aiStructuredOutputModels", String(p.aiStructuredOutputModelCount)]);
|
|
63
|
-
rows.push(["aiProvider", p.aiProvider ?? "none"]);
|
|
64
|
-
rows.push(["aiConnectionOk", p.aiConnectionOk === null ? "not tested" : String(p.aiConnectionOk)]);
|
|
65
|
-
rows.push(["pdfConverter", p.pdfConverter ?? "none"]);
|
|
66
|
-
rows.push(["pdfConverterOk", p.pdfConverterOk === null ? "not tested" : String(p.pdfConverterOk)]);
|
|
67
|
-
if (p.pdfConverterError) rows.push(["pdfConverterError", p.pdfConverterError]);
|
|
68
|
-
rows.push(["imageOcrPlatform", String(d.imageOcr.platformSupported)]);
|
|
69
|
-
rows.push(["imageOcrDependency", String(d.imageOcr.dependencyLoaded)]);
|
|
70
|
-
rows.push(["imageOcrOk", d.imageOcr.ocrOk === null ? "not tested" : String(d.imageOcr.ocrOk)]);
|
|
71
|
-
if (d.imageOcr.imagePath) rows.push(["imageOcrImage", d.imageOcr.imagePath]);
|
|
72
|
-
if (d.imageOcr.recognizedText) rows.push(["imageOcrText", d.imageOcr.recognizedText]);
|
|
73
|
-
if (typeof d.imageOcr.confidence === "number") rows.push(["imageOcrConfidence", `${(d.imageOcr.confidence * 100).toFixed(1)}%`]);
|
|
74
|
-
if (d.imageOcr.error) rows.push(["imageOcrError", d.imageOcr.error]);
|
|
75
|
-
rows.push(["hasDatabase", String(p.hasDatabase)]);
|
|
76
|
-
rows.push(["databaseTablesOk", p.databaseTablesOk === null ? "not tested" : String(p.databaseTablesOk)]);
|
|
77
|
-
if (p.missingDatabaseTables.length) rows.push(["missingDatabaseTables", p.missingDatabaseTables.join(", ")]);
|
|
78
|
-
rows.push(["migrations", String(p.migrationCount)]);
|
|
79
|
-
rows.push(["schemaValid", `${p.schemaValidCount}/${p.schemaCount}`]);
|
|
80
|
-
for (const invalid of p.invalidSchemas) rows.push(["invalidSchema", `${invalid.file}: ${invalid.error}`]);
|
|
81
|
-
for (const err of p.errors) rows.push(["error", err]);
|
|
82
|
-
return rows;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
//#endregion
|
|
86
12
|
//#region package.json
|
|
87
13
|
var name = "aiex-cli";
|
|
88
|
-
var version = "0.0.
|
|
14
|
+
var version = "0.0.7-beta.2";
|
|
89
15
|
var description = "JSON Schema → SQLite with AI-powered data extraction";
|
|
90
16
|
var package_default = {
|
|
91
17
|
name,
|
|
@@ -219,110 +145,7 @@ function seedConfig(config = createConfig()) {
|
|
|
219
145
|
}
|
|
220
146
|
|
|
221
147
|
//#endregion
|
|
222
|
-
//#region src/
|
|
223
|
-
const ModelCapabilitiesSchema = z.object({
|
|
224
|
-
vision: z.boolean(),
|
|
225
|
-
structuredOutput: z.boolean(),
|
|
226
|
-
maxTokens: z.number().int().positive().optional(),
|
|
227
|
-
maxOutputTokens: z.number().int().positive().optional()
|
|
228
|
-
});
|
|
229
|
-
const AIModelConfigSchema = z.object({
|
|
230
|
-
name: z.string().min(1),
|
|
231
|
-
capabilities: ModelCapabilitiesSchema
|
|
232
|
-
});
|
|
233
|
-
const AIProviderConfigSchema = z.object({
|
|
234
|
-
baseURL: z.string().min(1),
|
|
235
|
-
apiKey: z.string(),
|
|
236
|
-
models: z.array(AIModelConfigSchema).min(1),
|
|
237
|
-
timeout: z.number().int().positive().default(300).optional()
|
|
238
|
-
});
|
|
239
|
-
const PromptConfigSchema = z.object({
|
|
240
|
-
systemTemplate: z.string().min(1),
|
|
241
|
-
userTemplate: z.string().min(1)
|
|
242
|
-
});
|
|
243
|
-
const ExtractionConfigSchema = z.object({ outputDir: z.string().min(1) });
|
|
244
|
-
const ImageOcrFallbackSchema = z.preprocess((value) => [
|
|
245
|
-
"auto",
|
|
246
|
-
"off",
|
|
247
|
-
"local"
|
|
248
|
-
].includes(String(value)) ? "localAuto" : value, z.literal("localAuto").default("localAuto").optional());
|
|
249
|
-
const ImageOcrConfigSchema = z.object({
|
|
250
|
-
ocrFallback: ImageOcrFallbackSchema,
|
|
251
|
-
ocrLanguages: z.string().min(1).optional(),
|
|
252
|
-
ocrMinConfidence: z.number().min(0).max(1).optional()
|
|
253
|
-
});
|
|
254
|
-
const ExternalPdfConverterConfigSchema = z.object({
|
|
255
|
-
command: z.string().min(1),
|
|
256
|
-
args: z.array(z.string()).min(1).refine((args) => args.some((arg) => arg.includes("{input}")), { message: "args must contain {input} template variable" }),
|
|
257
|
-
outputFile: z.string().min(1).optional(),
|
|
258
|
-
timeout: z.number().int().positive().default(600).optional(),
|
|
259
|
-
fallbackToUnpdf: z.boolean().optional()
|
|
260
|
-
});
|
|
261
|
-
const MineruApiPdfConverterConfigSchema = z.object({
|
|
262
|
-
token: z.string(),
|
|
263
|
-
baseURL: z.string().url().optional(),
|
|
264
|
-
modelVersion: z.string().optional(),
|
|
265
|
-
isOcr: z.boolean().optional(),
|
|
266
|
-
enableFormula: z.boolean().optional(),
|
|
267
|
-
enableTable: z.boolean().optional()
|
|
268
|
-
});
|
|
269
|
-
const PdfConfigSchema = z.preprocess((value) => {
|
|
270
|
-
if (!value || typeof value !== "object") return value;
|
|
271
|
-
const config = { ...value };
|
|
272
|
-
if (config.converter === "markitdown" && config.markitdown) {
|
|
273
|
-
config.converter = "external";
|
|
274
|
-
config.external = config.markitdown;
|
|
275
|
-
} else if (config.converter === "marker" && config.marker) {
|
|
276
|
-
config.converter = "external";
|
|
277
|
-
config.external = config.marker;
|
|
278
|
-
} else if (config.converter === "markitdown" || config.converter === "marker") config.converter = "unpdf";
|
|
279
|
-
delete config.markitdown;
|
|
280
|
-
delete config.marker;
|
|
281
|
-
return config;
|
|
282
|
-
}, z.object({
|
|
283
|
-
converter: z.enum([
|
|
284
|
-
"unpdf",
|
|
285
|
-
"mineru",
|
|
286
|
-
"mineru_api",
|
|
287
|
-
"external"
|
|
288
|
-
]),
|
|
289
|
-
mineru: ExternalPdfConverterConfigSchema.optional(),
|
|
290
|
-
mineruApi: MineruApiPdfConverterConfigSchema.optional(),
|
|
291
|
-
external: ExternalPdfConverterConfigSchema.optional()
|
|
292
|
-
}));
|
|
293
|
-
const LangfuseConfigSchema = z.object({
|
|
294
|
-
publicKey: z.string(),
|
|
295
|
-
secretKey: z.string(),
|
|
296
|
-
host: z.string().optional()
|
|
297
|
-
});
|
|
298
|
-
const NotionSchemaConfigSchema = z.object({
|
|
299
|
-
databaseId: z.string(),
|
|
300
|
-
titleProperty: z.string().optional(),
|
|
301
|
-
fieldMap: z.record(z.string()).optional()
|
|
302
|
-
});
|
|
303
|
-
const NotionConfigSchema = z.object({
|
|
304
|
-
enabled: z.boolean(),
|
|
305
|
-
token: z.string(),
|
|
306
|
-
schemas: z.record(NotionSchemaConfigSchema).default({})
|
|
307
|
-
});
|
|
308
|
-
const WebhookConfigSchema = z.object({
|
|
309
|
-
enabled: z.boolean(),
|
|
310
|
-
url: z.string(),
|
|
311
|
-
secret: z.string().optional()
|
|
312
|
-
});
|
|
313
|
-
const AIConfigSchema = z.object({
|
|
314
|
-
provider: AIProviderConfigSchema,
|
|
315
|
-
prompt: PromptConfigSchema,
|
|
316
|
-
extraction: ExtractionConfigSchema,
|
|
317
|
-
image: ImageOcrConfigSchema.optional(),
|
|
318
|
-
pdf: PdfConfigSchema.optional(),
|
|
319
|
-
langfuse: LangfuseConfigSchema.optional(),
|
|
320
|
-
notion: NotionConfigSchema.optional(),
|
|
321
|
-
webhook: WebhookConfigSchema.optional()
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
//#endregion
|
|
325
|
-
//#region src/core/ai-extraction/types.ts
|
|
148
|
+
//#region src/domain/ai/types.ts
|
|
326
149
|
const PLACEHOLDER_SCHEMA = "{schema}";
|
|
327
150
|
const PLACEHOLDER_TEXT = "{text}";
|
|
328
151
|
const DEFAULT_MODELS = [{
|
|
@@ -395,63 +218,554 @@ const DEFAULT_AI_CONFIG = {
|
|
|
395
218
|
};
|
|
396
219
|
|
|
397
220
|
//#endregion
|
|
398
|
-
//#region src/
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
221
|
+
//#region src/domain/doctor/diagnostics.ts
|
|
222
|
+
function buildDoctorDiagnostics(input) {
|
|
223
|
+
return {
|
|
224
|
+
cli: {
|
|
225
|
+
name: input.pkg.name,
|
|
226
|
+
version: input.pkg.version,
|
|
227
|
+
executable: input.executable
|
|
228
|
+
},
|
|
229
|
+
runtime: {
|
|
230
|
+
node: input.node,
|
|
231
|
+
platform: input.platform,
|
|
232
|
+
arch: input.arch,
|
|
233
|
+
shell: input.shell,
|
|
234
|
+
packageManager: input.packageManager
|
|
235
|
+
},
|
|
236
|
+
system: {
|
|
237
|
+
os: `${input.osType} ${input.osRelease}`,
|
|
238
|
+
cwd: input.cwd
|
|
239
|
+
},
|
|
240
|
+
imageOcr: { ...input.imageOcr },
|
|
241
|
+
config: {
|
|
242
|
+
path: input.configPath,
|
|
243
|
+
keys: [...input.configStoreKeys].sort()
|
|
244
|
+
},
|
|
245
|
+
project: { ...input.project }
|
|
246
|
+
};
|
|
421
247
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const gitignorePath = path.join(projectRoot, GITIGNORE_FILE);
|
|
425
|
-
try {
|
|
426
|
-
const content = await fs.readFile(gitignorePath, "utf-8");
|
|
427
|
-
if (content.split("\n").some((line) => line.trim() === fileName || line.includes(".aiex/"))) return;
|
|
428
|
-
const newContent = content.endsWith("\n") ? `${content}${fileName}\n` : `${content}\n${fileName}\n`;
|
|
429
|
-
await fs.writeFile(gitignorePath, newContent);
|
|
430
|
-
} catch {
|
|
431
|
-
await fs.writeFile(gitignorePath, `${fileName}\n`);
|
|
432
|
-
}
|
|
248
|
+
function formatDoctorDiagnosticsJson(d) {
|
|
249
|
+
return `${JSON.stringify(d, null, 2)}\n`;
|
|
433
250
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
251
|
+
function doctorDiagnosticsTableRows(d) {
|
|
252
|
+
const rows = [
|
|
253
|
+
["node", d.runtime.node],
|
|
254
|
+
["platform", d.runtime.platform],
|
|
255
|
+
["arch", d.runtime.arch],
|
|
256
|
+
["shell", d.runtime.shell],
|
|
257
|
+
["packageManager", d.runtime.packageManager],
|
|
258
|
+
["os", d.system.os],
|
|
259
|
+
["cwd", d.system.cwd],
|
|
260
|
+
["config", d.config.path],
|
|
261
|
+
["configKeys", d.config.keys.join(", ")]
|
|
262
|
+
];
|
|
263
|
+
const p = d.project;
|
|
264
|
+
rows.push(["aiexDir", p.aiexDir]);
|
|
265
|
+
rows.push(["dirExists", String(p.dirExists)]);
|
|
266
|
+
rows.push(["schemaFiles", `${p.schemaCount} (${p.schemaFiles.join(", ") || "none"})`]);
|
|
267
|
+
rows.push(["aiConfig", String(p.aiConfig)]);
|
|
268
|
+
rows.push(["aiApiKeySet", String(p.aiApiKeySet)]);
|
|
269
|
+
rows.push(["aiModels", p.aiModelCount ? p.aiModels.join(", ") : "none"]);
|
|
270
|
+
rows.push(["aiVisionModels", String(p.aiVisionModelCount)]);
|
|
271
|
+
rows.push(["aiStructuredOutputModels", String(p.aiStructuredOutputModelCount)]);
|
|
272
|
+
rows.push(["aiProvider", p.aiProvider ?? "none"]);
|
|
273
|
+
rows.push(["aiConnectionOk", p.aiConnectionOk === null ? "not tested" : String(p.aiConnectionOk)]);
|
|
274
|
+
rows.push(["pdfConverter", p.pdfConverter ?? "none"]);
|
|
275
|
+
rows.push(["pdfConverterOk", p.pdfConverterOk === null ? "not tested" : String(p.pdfConverterOk)]);
|
|
276
|
+
if (p.pdfConverterError) rows.push(["pdfConverterError", p.pdfConverterError]);
|
|
277
|
+
rows.push(["imageOcrPlatform", String(d.imageOcr.platformSupported)]);
|
|
278
|
+
rows.push(["imageOcrDependency", String(d.imageOcr.dependencyLoaded)]);
|
|
279
|
+
rows.push(["imageOcrOk", d.imageOcr.ocrOk === null ? "not tested" : String(d.imageOcr.ocrOk)]);
|
|
280
|
+
if (d.imageOcr.imagePath) rows.push(["imageOcrImage", d.imageOcr.imagePath]);
|
|
281
|
+
if (d.imageOcr.recognizedText) rows.push(["imageOcrText", d.imageOcr.recognizedText]);
|
|
282
|
+
if (typeof d.imageOcr.confidence === "number") rows.push(["imageOcrConfidence", `${(d.imageOcr.confidence * 100).toFixed(1)}%`]);
|
|
283
|
+
if (d.imageOcr.error) rows.push(["imageOcrError", d.imageOcr.error]);
|
|
284
|
+
rows.push(["hasDatabase", String(p.hasDatabase)]);
|
|
285
|
+
rows.push(["databaseTablesOk", p.databaseTablesOk === null ? "not tested" : String(p.databaseTablesOk)]);
|
|
286
|
+
if (p.missingDatabaseTables.length) rows.push(["missingDatabaseTables", p.missingDatabaseTables.join(", ")]);
|
|
287
|
+
rows.push(["migrations", String(p.migrationCount)]);
|
|
288
|
+
rows.push(["schemaValid", `${p.schemaValidCount}/${p.schemaCount}`]);
|
|
289
|
+
for (const invalid of p.invalidSchemas) rows.push(["invalidSchema", `${invalid.file}: ${invalid.error}`]);
|
|
290
|
+
for (const err of p.errors) rows.push(["error", err]);
|
|
291
|
+
return rows;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/domain/schema/parser.ts
|
|
296
|
+
function toSnakeCase(str) {
|
|
297
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
298
|
+
}
|
|
299
|
+
function mapColumnType(property) {
|
|
300
|
+
switch (property.type) {
|
|
301
|
+
case "string": {
|
|
302
|
+
const format = property.format;
|
|
303
|
+
if (format === "date-time" || property.drizzle?.mode === "timestamp") return {
|
|
304
|
+
class: "integer",
|
|
305
|
+
mode: "timestamp"
|
|
306
|
+
};
|
|
307
|
+
if (format === "json" || property.drizzle?.mode === "json") return {
|
|
308
|
+
class: "text",
|
|
309
|
+
mode: "json"
|
|
310
|
+
};
|
|
311
|
+
return { class: "text" };
|
|
312
|
+
}
|
|
313
|
+
case "integer": {
|
|
314
|
+
const mode = property.drizzle?.mode;
|
|
315
|
+
if (mode === "boolean" || mode === "timestamp" || mode === "timestamp_ms" || mode === "bigint") return {
|
|
316
|
+
class: "integer",
|
|
317
|
+
mode
|
|
318
|
+
};
|
|
319
|
+
return { class: "integer" };
|
|
320
|
+
}
|
|
321
|
+
case "number": return { class: "real" };
|
|
322
|
+
case "boolean": return {
|
|
323
|
+
class: "integer",
|
|
324
|
+
mode: "boolean"
|
|
325
|
+
};
|
|
326
|
+
case "object":
|
|
327
|
+
case "array": return {
|
|
328
|
+
class: "text",
|
|
329
|
+
mode: "json"
|
|
330
|
+
};
|
|
331
|
+
case "null":
|
|
332
|
+
default: return { class: "text" };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function mapPropertyToColumn(name$1, property, isRequired) {
|
|
336
|
+
return {
|
|
337
|
+
name: toSnakeCase(name$1),
|
|
338
|
+
columnType: mapColumnType(property),
|
|
339
|
+
isPrimary: property.primary ?? false,
|
|
340
|
+
isAutoIncrement: property.autoIncrement ?? false,
|
|
341
|
+
isNullable: !isRequired && !property.primary,
|
|
342
|
+
isUnique: property.unique ?? false,
|
|
343
|
+
default: property.default,
|
|
344
|
+
isForeignKey: property.foreignKey !== void 0,
|
|
345
|
+
foreignKeyRef: property.foreignKey ?? void 0
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function getColumnChecks(prop, colName) {
|
|
349
|
+
const checks = [];
|
|
350
|
+
if (prop.type === "string") {
|
|
351
|
+
if (prop.minLength !== void 0 && prop.minLength > 0) checks.push({
|
|
352
|
+
name: `${colName}_min_length`,
|
|
353
|
+
column: colName,
|
|
354
|
+
kind: "min_length",
|
|
355
|
+
value: prop.minLength
|
|
356
|
+
});
|
|
357
|
+
if (prop.maxLength !== void 0) checks.push({
|
|
358
|
+
name: `${colName}_max_length`,
|
|
359
|
+
column: colName,
|
|
360
|
+
kind: "max_length",
|
|
361
|
+
value: prop.maxLength
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (prop.type === "integer" || prop.type === "number") {
|
|
365
|
+
if (prop.minimum !== void 0) checks.push({
|
|
366
|
+
name: `${colName}_min`,
|
|
367
|
+
column: colName,
|
|
368
|
+
kind: "min_value",
|
|
369
|
+
value: prop.minimum
|
|
370
|
+
});
|
|
371
|
+
if (prop.maximum !== void 0) checks.push({
|
|
372
|
+
name: `${colName}_max`,
|
|
373
|
+
column: colName,
|
|
374
|
+
kind: "max_value",
|
|
375
|
+
value: prop.maximum
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return checks;
|
|
379
|
+
}
|
|
380
|
+
function parseObjectToTable(schema, _warnings) {
|
|
381
|
+
const tableName = schema.table.name;
|
|
382
|
+
const columns = [];
|
|
383
|
+
const checks = [];
|
|
384
|
+
const requiredFields = new Set(schema.required ?? []);
|
|
385
|
+
const autoColumns = /* @__PURE__ */ new Set();
|
|
386
|
+
if (schema.table.timestamps) {
|
|
387
|
+
autoColumns.add("created_at");
|
|
388
|
+
autoColumns.add("updated_at");
|
|
389
|
+
}
|
|
390
|
+
if (schema.table.softDelete) autoColumns.add("deleted_at");
|
|
391
|
+
for (const [propName, prop] of Object.entries(schema.properties)) {
|
|
392
|
+
if (prop.nested?.enabled && (prop.type === "object" || prop.type === "array")) continue;
|
|
393
|
+
if (prop.type === "array" && prop.items?.nested?.enabled) continue;
|
|
394
|
+
const snakeName = toSnakeCase(propName);
|
|
395
|
+
if (autoColumns.has(snakeName)) continue;
|
|
396
|
+
const column = mapPropertyToColumn(propName, prop, requiredFields.has(propName));
|
|
397
|
+
columns.push(column);
|
|
398
|
+
checks.push(...getColumnChecks(prop, column.name));
|
|
399
|
+
}
|
|
400
|
+
if (schema.table.timestamps) {
|
|
401
|
+
const tsCol = {
|
|
402
|
+
name: "created_at",
|
|
403
|
+
columnType: {
|
|
404
|
+
class: "integer",
|
|
405
|
+
mode: "timestamp"
|
|
406
|
+
},
|
|
407
|
+
isPrimary: false,
|
|
408
|
+
isAutoIncrement: false,
|
|
409
|
+
isNullable: false,
|
|
410
|
+
isUnique: false
|
|
411
|
+
};
|
|
412
|
+
columns.push(tsCol);
|
|
413
|
+
columns.push({
|
|
414
|
+
...tsCol,
|
|
415
|
+
name: "updated_at"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (schema.table.softDelete) columns.push({
|
|
419
|
+
name: "deleted_at",
|
|
420
|
+
columnType: {
|
|
421
|
+
class: "integer",
|
|
422
|
+
mode: "timestamp"
|
|
423
|
+
},
|
|
424
|
+
isPrimary: false,
|
|
425
|
+
isAutoIncrement: false,
|
|
426
|
+
isNullable: true,
|
|
427
|
+
isUnique: false
|
|
428
|
+
});
|
|
429
|
+
return checks.length > 0 ? {
|
|
430
|
+
name: tableName,
|
|
431
|
+
columns,
|
|
432
|
+
checks
|
|
433
|
+
} : {
|
|
434
|
+
name: tableName,
|
|
435
|
+
columns
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function parseNestedObject(propName, property, parentTableName, warnings) {
|
|
439
|
+
const nestedTableName = `${parentTableName}_${toSnakeCase(propName)}`;
|
|
440
|
+
const columns = [];
|
|
441
|
+
const checks = [];
|
|
442
|
+
const relationType = property.nested?.relation === "has-many" ? "has-many" : "has-one";
|
|
443
|
+
columns.push({
|
|
444
|
+
name: "id",
|
|
445
|
+
columnType: { class: "integer" },
|
|
446
|
+
isPrimary: true,
|
|
447
|
+
isAutoIncrement: true,
|
|
448
|
+
isNullable: false,
|
|
449
|
+
isUnique: false
|
|
450
|
+
});
|
|
451
|
+
columns.push({
|
|
452
|
+
name: `${parentTableName}_id`,
|
|
453
|
+
columnType: { class: "integer" },
|
|
454
|
+
isPrimary: false,
|
|
455
|
+
isAutoIncrement: false,
|
|
456
|
+
isNullable: false,
|
|
457
|
+
isUnique: false,
|
|
458
|
+
isForeignKey: true,
|
|
459
|
+
foreignKeyRef: {
|
|
460
|
+
table: parentTableName,
|
|
461
|
+
column: "id"
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
if (property.type === "object" && property.properties) for (const [childName, childProp] of Object.entries(property.properties)) {
|
|
465
|
+
if (childProp.nested?.enabled) {
|
|
466
|
+
warnings.push(`Nested property "${childName}" inside "${nestedTableName}" is skipped — only one level of nesting is supported. Remove nested.enabled or use drizzle.mode: 'json' instead.`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const column = mapPropertyToColumn(childName, childProp, false);
|
|
470
|
+
columns.push(column);
|
|
471
|
+
checks.push(...getColumnChecks(childProp, column.name));
|
|
472
|
+
}
|
|
473
|
+
const relation = {
|
|
474
|
+
fromTable: nestedTableName,
|
|
475
|
+
fromColumn: `${parentTableName}_id`,
|
|
476
|
+
toTable: parentTableName,
|
|
477
|
+
toColumn: "id",
|
|
478
|
+
name: parentTableName
|
|
479
|
+
};
|
|
480
|
+
const reverseRelation = {
|
|
481
|
+
type: relationType,
|
|
482
|
+
fromTable: parentTableName,
|
|
483
|
+
toTable: nestedTableName,
|
|
484
|
+
name: toSnakeCase(propName)
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
table: checks.length > 0 ? {
|
|
488
|
+
name: nestedTableName,
|
|
489
|
+
columns,
|
|
490
|
+
checks
|
|
491
|
+
} : {
|
|
492
|
+
name: nestedTableName,
|
|
493
|
+
columns
|
|
494
|
+
},
|
|
495
|
+
relation,
|
|
496
|
+
reverseRelation
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function parseJsonSchema(schema) {
|
|
500
|
+
const tables = [];
|
|
501
|
+
const relations = [];
|
|
502
|
+
const reverseRelations = [];
|
|
503
|
+
const warnings = [];
|
|
504
|
+
const mainTable = parseObjectToTable(schema, warnings);
|
|
505
|
+
tables.push(mainTable);
|
|
506
|
+
for (const [propName, prop] of Object.entries(schema.properties)) if (prop.type === "object" && prop.nested?.enabled) {
|
|
507
|
+
const nested = parseNestedObject(propName, prop, mainTable.name, warnings);
|
|
508
|
+
tables.push(nested.table);
|
|
509
|
+
relations.push(nested.relation);
|
|
510
|
+
reverseRelations.push(nested.reverseRelation);
|
|
511
|
+
} else if (prop.type === "array" && prop.items?.nested?.enabled && prop.items?.type === "object" && prop.items.properties) {
|
|
512
|
+
const nested = parseNestedObject(propName, prop.items, mainTable.name, warnings);
|
|
513
|
+
tables.push(nested.table);
|
|
514
|
+
relations.push(nested.relation);
|
|
515
|
+
reverseRelations.push(nested.reverseRelation);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
tables,
|
|
519
|
+
relations,
|
|
520
|
+
reverseRelations,
|
|
521
|
+
warnings
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/domain/schema/schemas.ts
|
|
527
|
+
const DrizzleModeSchema = z.enum([
|
|
528
|
+
"json",
|
|
529
|
+
"timestamp",
|
|
530
|
+
"timestamp_ms",
|
|
531
|
+
"boolean",
|
|
532
|
+
"bigint"
|
|
533
|
+
]);
|
|
534
|
+
const DrizzleExtensionSchema = z.object({
|
|
535
|
+
mode: DrizzleModeSchema.optional(),
|
|
536
|
+
customType: z.string().optional()
|
|
537
|
+
}).optional();
|
|
538
|
+
const NestedConfigSchema = z.object({
|
|
539
|
+
enabled: z.literal(true),
|
|
540
|
+
relation: z.enum(["has-one", "has-many"])
|
|
541
|
+
});
|
|
542
|
+
const ForeignKeyRefSchema = z.object({
|
|
543
|
+
table: z.string().min(1),
|
|
544
|
+
column: z.string().min(1)
|
|
545
|
+
});
|
|
546
|
+
const JsonSchemaPropertySchema = z.lazy(() => z.object({
|
|
547
|
+
description: z.string().optional(),
|
|
548
|
+
type: z.enum([
|
|
549
|
+
"string",
|
|
550
|
+
"integer",
|
|
551
|
+
"number",
|
|
552
|
+
"boolean",
|
|
553
|
+
"object",
|
|
554
|
+
"array",
|
|
555
|
+
"null"
|
|
556
|
+
]),
|
|
557
|
+
format: z.string().optional(),
|
|
558
|
+
pattern: z.string().optional(),
|
|
559
|
+
enum: z.array(z.union([z.string(), z.number()])).optional(),
|
|
560
|
+
primary: z.boolean().optional(),
|
|
561
|
+
autoIncrement: z.boolean().optional(),
|
|
562
|
+
unique: z.boolean().optional(),
|
|
563
|
+
default: z.unknown().optional(),
|
|
564
|
+
maxLength: z.number().int().positive().optional(),
|
|
565
|
+
minLength: z.number().int().nonnegative().optional(),
|
|
566
|
+
minimum: z.number().optional(),
|
|
567
|
+
maximum: z.number().optional(),
|
|
568
|
+
examples: z.array(z.unknown()).optional(),
|
|
569
|
+
xPrompt: z.string().optional(),
|
|
570
|
+
drizzle: DrizzleExtensionSchema,
|
|
571
|
+
nested: NestedConfigSchema.optional(),
|
|
572
|
+
foreignKey: ForeignKeyRefSchema.optional(),
|
|
573
|
+
properties: z.record(z.string(), JsonSchemaPropertySchema).optional(),
|
|
574
|
+
items: JsonSchemaPropertySchema.optional(),
|
|
575
|
+
required: z.array(z.string()).optional()
|
|
576
|
+
}));
|
|
577
|
+
const TableConfigSchema = z.object({
|
|
578
|
+
name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "Table name must be snake_case (lowercase letters, digits, underscores)"),
|
|
579
|
+
timestamps: z.boolean().optional(),
|
|
580
|
+
softDelete: z.boolean().optional()
|
|
581
|
+
});
|
|
582
|
+
const ExamplePairSchema = z.object({
|
|
583
|
+
text: z.string().min(1),
|
|
584
|
+
output: z.record(z.string(), z.unknown())
|
|
585
|
+
});
|
|
586
|
+
const JsonSchemaDefinitionSchema = z.object({
|
|
587
|
+
$schema: z.string().optional(),
|
|
588
|
+
title: z.string().min(1),
|
|
589
|
+
description: z.string().optional(),
|
|
590
|
+
type: z.literal("object"),
|
|
591
|
+
table: TableConfigSchema,
|
|
592
|
+
properties: z.record(z.string(), JsonSchemaPropertySchema),
|
|
593
|
+
required: z.array(z.string()).optional(),
|
|
594
|
+
examples: z.array(ExamplePairSchema).optional()
|
|
595
|
+
}).refine((schema) => Object.keys(schema.properties).length >= 1, {
|
|
596
|
+
message: "At least one property is required",
|
|
597
|
+
path: ["properties"]
|
|
598
|
+
}).refine((schema) => !schema.required || schema.required.every((r) => r in schema.properties), {
|
|
599
|
+
message: "All required fields must be defined in properties",
|
|
600
|
+
path: ["required"]
|
|
601
|
+
}).refine((schema) => {
|
|
602
|
+
return !Object.values(schema.properties).some((p) => p.primary) || Object.values(schema.properties).filter((p) => p.primary).length === 1;
|
|
603
|
+
}, {
|
|
604
|
+
message: "Only one primary key is allowed per table",
|
|
605
|
+
path: ["properties"]
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/domain/ai/schemas.ts
|
|
610
|
+
const ModelCapabilitiesSchema = z.object({
|
|
611
|
+
vision: z.boolean(),
|
|
612
|
+
structuredOutput: z.boolean(),
|
|
613
|
+
maxTokens: z.number().int().positive().optional(),
|
|
614
|
+
maxOutputTokens: z.number().int().positive().optional()
|
|
615
|
+
});
|
|
616
|
+
const AIModelConfigSchema = z.object({
|
|
617
|
+
name: z.string().min(1),
|
|
618
|
+
capabilities: ModelCapabilitiesSchema
|
|
619
|
+
});
|
|
620
|
+
const AIProviderConfigSchema = z.object({
|
|
621
|
+
baseURL: z.string().min(1),
|
|
622
|
+
apiKey: z.string(),
|
|
623
|
+
models: z.array(AIModelConfigSchema).min(1),
|
|
624
|
+
timeout: z.number().int().positive().default(300).optional()
|
|
625
|
+
});
|
|
626
|
+
const PromptConfigSchema = z.object({
|
|
627
|
+
systemTemplate: z.string().min(1),
|
|
628
|
+
userTemplate: z.string().min(1)
|
|
629
|
+
});
|
|
630
|
+
const ExtractionConfigSchema = z.object({ outputDir: z.string().min(1) });
|
|
631
|
+
const ImageOcrFallbackSchema = z.preprocess((value) => [
|
|
632
|
+
"auto",
|
|
633
|
+
"off",
|
|
634
|
+
"local"
|
|
635
|
+
].includes(String(value)) ? "localAuto" : value, z.literal("localAuto").default("localAuto").optional());
|
|
636
|
+
const ImageOcrConfigSchema = z.object({
|
|
637
|
+
ocrFallback: ImageOcrFallbackSchema,
|
|
638
|
+
ocrLanguages: z.string().min(1).optional(),
|
|
639
|
+
ocrMinConfidence: z.number().min(0).max(1).optional()
|
|
640
|
+
});
|
|
641
|
+
const ExternalPdfConverterConfigSchema = z.object({
|
|
642
|
+
command: z.string().min(1),
|
|
643
|
+
args: z.array(z.string()).min(1).refine((args) => args.some((arg) => arg.includes("{input}")), { message: "args must contain {input} template variable" }),
|
|
644
|
+
outputFile: z.string().min(1).optional(),
|
|
645
|
+
timeout: z.number().int().positive().default(600).optional(),
|
|
646
|
+
fallbackToUnpdf: z.boolean().optional()
|
|
647
|
+
});
|
|
648
|
+
const MineruApiPdfConverterConfigSchema = z.object({
|
|
649
|
+
token: z.string(),
|
|
650
|
+
baseURL: z.string().url().optional(),
|
|
651
|
+
modelVersion: z.string().optional(),
|
|
652
|
+
isOcr: z.boolean().optional(),
|
|
653
|
+
enableFormula: z.boolean().optional(),
|
|
654
|
+
enableTable: z.boolean().optional()
|
|
655
|
+
});
|
|
656
|
+
const PdfConfigSchema = z.preprocess((value) => {
|
|
657
|
+
if (!value || typeof value !== "object") return value;
|
|
658
|
+
const config = { ...value };
|
|
659
|
+
if (config.converter === "markitdown" && config.markitdown) {
|
|
660
|
+
config.converter = "external";
|
|
661
|
+
config.external = config.markitdown;
|
|
662
|
+
} else if (config.converter === "marker" && config.marker) {
|
|
663
|
+
config.converter = "external";
|
|
664
|
+
config.external = config.marker;
|
|
665
|
+
} else if (config.converter === "markitdown" || config.converter === "marker") config.converter = "unpdf";
|
|
666
|
+
delete config.markitdown;
|
|
667
|
+
delete config.marker;
|
|
668
|
+
return config;
|
|
669
|
+
}, z.object({
|
|
670
|
+
converter: z.enum([
|
|
671
|
+
"unpdf",
|
|
672
|
+
"mineru",
|
|
673
|
+
"mineru_api",
|
|
674
|
+
"external"
|
|
675
|
+
]),
|
|
676
|
+
mineru: ExternalPdfConverterConfigSchema.optional(),
|
|
677
|
+
mineruApi: MineruApiPdfConverterConfigSchema.optional(),
|
|
678
|
+
external: ExternalPdfConverterConfigSchema.optional()
|
|
679
|
+
}));
|
|
680
|
+
const LangfuseConfigSchema = z.object({
|
|
681
|
+
publicKey: z.string(),
|
|
682
|
+
secretKey: z.string(),
|
|
683
|
+
host: z.string().optional()
|
|
684
|
+
});
|
|
685
|
+
const NotionSchemaConfigSchema = z.object({
|
|
686
|
+
databaseId: z.string(),
|
|
687
|
+
titleProperty: z.string().optional(),
|
|
688
|
+
fieldMap: z.record(z.string()).optional()
|
|
689
|
+
});
|
|
690
|
+
const NotionConfigSchema = z.object({
|
|
691
|
+
enabled: z.boolean(),
|
|
692
|
+
token: z.string(),
|
|
693
|
+
schemas: z.record(NotionSchemaConfigSchema).default({})
|
|
694
|
+
});
|
|
695
|
+
const WebhookConfigSchema = z.object({
|
|
696
|
+
enabled: z.boolean(),
|
|
697
|
+
url: z.string(),
|
|
698
|
+
secret: z.string().optional()
|
|
699
|
+
});
|
|
700
|
+
const AIConfigSchema = z.object({
|
|
701
|
+
provider: AIProviderConfigSchema,
|
|
702
|
+
prompt: PromptConfigSchema,
|
|
703
|
+
extraction: ExtractionConfigSchema,
|
|
704
|
+
image: ImageOcrConfigSchema.optional(),
|
|
705
|
+
pdf: PdfConfigSchema.optional(),
|
|
706
|
+
langfuse: LangfuseConfigSchema.optional(),
|
|
707
|
+
notion: NotionConfigSchema.optional(),
|
|
708
|
+
webhook: WebhookConfigSchema.optional()
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/infrastructure/ai/ai-config-store.ts
|
|
713
|
+
const CONFIG_FILE_NAME = "ai-config.json";
|
|
714
|
+
const GITIGNORE_FILE = ".gitignore";
|
|
715
|
+
async function readAIConfig(aiexDir) {
|
|
716
|
+
const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
|
|
717
|
+
try {
|
|
718
|
+
const parsed = await readFile(configPath);
|
|
719
|
+
return AIConfigSchema.parse(parsed);
|
|
720
|
+
} catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function writeAIConfig(aiexDir, config) {
|
|
725
|
+
const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
|
|
726
|
+
await fs.mkdir(aiexDir, { recursive: true });
|
|
727
|
+
await writeFile(configPath, config, {
|
|
728
|
+
spaces: 2,
|
|
729
|
+
EOL: "\n"
|
|
730
|
+
});
|
|
731
|
+
await addToGitignore(aiexDir, CONFIG_FILE_NAME);
|
|
732
|
+
}
|
|
733
|
+
function getDefaultAIConfig() {
|
|
734
|
+
return structuredClone(DEFAULT_AI_CONFIG);
|
|
735
|
+
}
|
|
736
|
+
async function addToGitignore(aiexDir, fileName) {
|
|
737
|
+
const projectRoot = path.dirname(aiexDir);
|
|
738
|
+
const gitignorePath = path.join(projectRoot, GITIGNORE_FILE);
|
|
739
|
+
try {
|
|
740
|
+
const content = await fs.readFile(gitignorePath, "utf-8");
|
|
741
|
+
if (content.split("\n").some((line) => line.trim() === fileName || line.includes(".aiex/"))) return;
|
|
742
|
+
const newContent = content.endsWith("\n") ? `${content}${fileName}\n` : `${content}\n${fileName}\n`;
|
|
743
|
+
await fs.writeFile(gitignorePath, newContent);
|
|
744
|
+
} catch {
|
|
745
|
+
await fs.writeFile(gitignorePath, `${fileName}\n`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
//#endregion
|
|
750
|
+
//#region src/locales/en.ts
|
|
751
|
+
const en = {
|
|
752
|
+
common: {
|
|
753
|
+
done: "Done!",
|
|
754
|
+
failed: "Failed!",
|
|
755
|
+
cancelled: "Cancelled",
|
|
756
|
+
save: "Save",
|
|
757
|
+
cancel: "Cancel",
|
|
758
|
+
delete: "Delete",
|
|
759
|
+
close: "Close",
|
|
760
|
+
loading: "Loading...",
|
|
761
|
+
unknownError: "Unknown error"
|
|
762
|
+
},
|
|
763
|
+
cli: { description: "JSON Schema → SQLite with AI-powered data extraction" },
|
|
764
|
+
command: {
|
|
765
|
+
web: {
|
|
766
|
+
description: "Start visual JSON Schema editor",
|
|
767
|
+
args: { port: "Port to listen on" },
|
|
768
|
+
starting: "Starting web server...",
|
|
455
769
|
serverRunning: "Server running at {{url}}",
|
|
456
770
|
schemaDir: "Schema directory: {{path}}",
|
|
457
771
|
pressCtrlC: "Press Ctrl+C to stop",
|
|
@@ -959,7 +1273,7 @@ function t(key, options) {
|
|
|
959
1273
|
}
|
|
960
1274
|
|
|
961
1275
|
//#endregion
|
|
962
|
-
//#region src/
|
|
1276
|
+
//#region src/infrastructure/ocr/system-ocr.ts
|
|
963
1277
|
const DEFAULT_OCR_LANGUAGES = "en-US, zh-Hans";
|
|
964
1278
|
const SELF_CHECK_EXPECTED_TEXT = "AIEX";
|
|
965
1279
|
const defaultRuntime = {
|
|
@@ -983,417 +1297,87 @@ function shouldUseImageOcrFallback(aiConfig, modelOverride, runtime = defaultRun
|
|
|
983
1297
|
function isLocalOcrPlatform(platform) {
|
|
984
1298
|
return platform === "darwin" || platform === "win32";
|
|
985
1299
|
}
|
|
986
|
-
function parseOcrLanguages(languages) {
|
|
987
|
-
return (languages ?? DEFAULT_OCR_LANGUAGES).split(",").map((language) => language.trim()).filter(Boolean);
|
|
988
|
-
}
|
|
989
|
-
async function recognizeImageText(imagePath, config, runtime = defaultRuntime) {
|
|
990
|
-
if (!isLocalOcrPlatform(runtime.platform)) throw new Error(t("errors.ocr.platformUnsupported", { platform: runtime.platform }));
|
|
991
|
-
let localOcr;
|
|
992
|
-
try {
|
|
993
|
-
localOcr = await runtime.loadLocalOcr();
|
|
994
|
-
} catch (error) {
|
|
995
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
996
|
-
throw new Error(t("errors.ocr.unavailable", { error: message }));
|
|
997
|
-
}
|
|
998
|
-
const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, parseOcrLanguages(config?.ocrLanguages));
|
|
999
|
-
const text = result.text.trim();
|
|
1000
|
-
if (!text) throw new Error(t("errors.ocr.noText"));
|
|
1001
|
-
const confidence = result.confidence;
|
|
1002
|
-
const minConfidence = config?.ocrMinConfidence ?? 0;
|
|
1003
|
-
if (confidence < minConfidence) throw new Error(t("errors.ocr.lowConfidence", {
|
|
1004
|
-
confidence: (confidence * 100).toFixed(1),
|
|
1005
|
-
min: (minConfidence * 100).toFixed(1)
|
|
1006
|
-
}));
|
|
1007
|
-
return {
|
|
1008
|
-
text,
|
|
1009
|
-
confidence
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
function normalizeOcrText(text) {
|
|
1013
|
-
return text.replace(/\s+/g, "").trim().toUpperCase();
|
|
1014
|
-
}
|
|
1015
|
-
async function checkImageOcrAvailability(imagePath, runtime = defaultRuntime) {
|
|
1016
|
-
if (!isLocalOcrPlatform(runtime.platform)) return {
|
|
1017
|
-
platformSupported: false,
|
|
1018
|
-
dependencyLoaded: false,
|
|
1019
|
-
ocrOk: null,
|
|
1020
|
-
imagePath,
|
|
1021
|
-
error: t("errors.ocr.platformUnsupported", { platform: runtime.platform })
|
|
1022
|
-
};
|
|
1023
|
-
let localOcr;
|
|
1024
|
-
try {
|
|
1025
|
-
localOcr = await runtime.loadLocalOcr();
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
return {
|
|
1028
|
-
platformSupported: true,
|
|
1029
|
-
dependencyLoaded: false,
|
|
1030
|
-
ocrOk: null,
|
|
1031
|
-
imagePath,
|
|
1032
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
if (!imagePath) return {
|
|
1036
|
-
platformSupported: true,
|
|
1037
|
-
dependencyLoaded: true,
|
|
1038
|
-
ocrOk: null,
|
|
1039
|
-
error: "No OCR self-check image was found."
|
|
1040
|
-
};
|
|
1041
|
-
try {
|
|
1042
|
-
const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, ["en-US"]);
|
|
1043
|
-
const recognizedText = result.text.trim();
|
|
1044
|
-
const ocrOk = normalizeOcrText(recognizedText).includes(SELF_CHECK_EXPECTED_TEXT);
|
|
1045
|
-
return {
|
|
1046
|
-
platformSupported: true,
|
|
1047
|
-
dependencyLoaded: true,
|
|
1048
|
-
ocrOk,
|
|
1049
|
-
imagePath,
|
|
1050
|
-
recognizedText,
|
|
1051
|
-
confidence: result.confidence,
|
|
1052
|
-
error: ocrOk ? void 0 : `Expected OCR text "${SELF_CHECK_EXPECTED_TEXT}" was not recognized.`
|
|
1053
|
-
};
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
return {
|
|
1056
|
-
platformSupported: true,
|
|
1057
|
-
dependencyLoaded: true,
|
|
1058
|
-
ocrOk: false,
|
|
1059
|
-
imagePath,
|
|
1060
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
//#endregion
|
|
1066
|
-
//#region src/core/schema-sqlite/generator.ts
|
|
1067
|
-
function generateColumnDefinition(column) {
|
|
1068
|
-
if (column.isPrimary && column.isAutoIncrement) return ` ${column.name}: integer().primaryKey({ autoIncrement: true })`;
|
|
1069
|
-
let def = ` ${column.name}: ${column.drizzleType}`;
|
|
1070
|
-
if (column.isPrimary) def += ".primaryKey()";
|
|
1071
|
-
if (!column.isNullable && !column.isPrimary) def += ".notNull()";
|
|
1072
|
-
if (column.isUnique && !column.isPrimary) def += ".unique()";
|
|
1073
|
-
if (column.defaultValue !== void 0) def += `.default(${column.defaultValue})`;
|
|
1074
|
-
if (column.isForeignKey && column.foreignKeyRef) def += `.references(() => ${column.foreignKeyRef.table}.${column.foreignKeyRef.column})`;
|
|
1075
|
-
return def;
|
|
1076
|
-
}
|
|
1077
|
-
function generateTableDefinition(table) {
|
|
1078
|
-
const columns = table.columns.map(generateColumnDefinition);
|
|
1079
|
-
return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n})`;
|
|
1080
|
-
}
|
|
1081
|
-
function generateRelationDefinitions(relations, reverseRelations) {
|
|
1082
|
-
if (relations.length === 0 && reverseRelations.length === 0) return "";
|
|
1083
|
-
const definitions = [];
|
|
1084
|
-
const childByTable = /* @__PURE__ */ new Map();
|
|
1085
|
-
for (const rel of relations) {
|
|
1086
|
-
const list = childByTable.get(rel.fromTable) ?? [];
|
|
1087
|
-
list.push(rel);
|
|
1088
|
-
childByTable.set(rel.fromTable, list);
|
|
1089
|
-
}
|
|
1090
|
-
const parentByTable = /* @__PURE__ */ new Map();
|
|
1091
|
-
for (const rel of reverseRelations) {
|
|
1092
|
-
const list = parentByTable.get(rel.fromTable) ?? [];
|
|
1093
|
-
list.push(rel);
|
|
1094
|
-
parentByTable.set(rel.fromTable, list);
|
|
1095
|
-
}
|
|
1096
|
-
const allTableNames = new Set([...childByTable.keys(), ...parentByTable.keys()]);
|
|
1097
|
-
for (const tableName of allTableNames) {
|
|
1098
|
-
const childRels = childByTable.get(tableName) ?? [];
|
|
1099
|
-
const parentRels = parentByTable.get(tableName) ?? [];
|
|
1100
|
-
const needsOne = childRels.length > 0 || parentRels.some((r) => r.type === "has-one");
|
|
1101
|
-
const needsMany = parentRels.some((r) => r.type === "has-many");
|
|
1102
|
-
const imports = [];
|
|
1103
|
-
if (needsOne) imports.push("one");
|
|
1104
|
-
if (needsMany) imports.push("many");
|
|
1105
|
-
const importStr = imports.join(", ");
|
|
1106
|
-
const relDefs = [];
|
|
1107
|
-
for (const rel of childRels) relDefs.push(` ${rel.name}: one(${rel.toTable}, {\n fields: [${rel.fromTable}.${rel.fromColumn}],\n references: [${rel.toTable}.${rel.toColumn}],\n })`);
|
|
1108
|
-
for (const rel of parentRels) if (rel.type === "has-many") relDefs.push(` ${rel.name}: many(${rel.toTable})`);
|
|
1109
|
-
else relDefs.push(` ${rel.name}: one(${rel.toTable})`);
|
|
1110
|
-
definitions.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${importStr} }) => ({\n${relDefs.join(",\n")}\n}))`);
|
|
1111
|
-
}
|
|
1112
|
-
return definitions.join("\n\n");
|
|
1113
|
-
}
|
|
1114
|
-
function generateDrizzleSchema(result) {
|
|
1115
|
-
const imports = `import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'`;
|
|
1116
|
-
const tableDefs = result.tables.map(generateTableDefinition).join("\n\n");
|
|
1117
|
-
const relationDefs = generateRelationDefinitions(result.relations, result.reverseRelations);
|
|
1118
|
-
const parts = [
|
|
1119
|
-
imports,
|
|
1120
|
-
"",
|
|
1121
|
-
tableDefs
|
|
1122
|
-
];
|
|
1123
|
-
if (relationDefs) parts.push("", relationDefs);
|
|
1124
|
-
parts.push("");
|
|
1125
|
-
return parts.join("\n");
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
//#endregion
|
|
1129
|
-
//#region src/core/schema-sqlite/parser.ts
|
|
1130
|
-
function toSnakeCase(str) {
|
|
1131
|
-
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1132
|
-
}
|
|
1133
|
-
function mapPropertyToColumn(name$1, property, isRequired) {
|
|
1134
|
-
const snakeName = toSnakeCase(name$1);
|
|
1135
|
-
let drizzleType;
|
|
1136
|
-
const isPrimary = property.primary ?? false;
|
|
1137
|
-
const isAutoIncrement = property.autoIncrement ?? false;
|
|
1138
|
-
switch (property.type) {
|
|
1139
|
-
case "string": {
|
|
1140
|
-
const format = property.format;
|
|
1141
|
-
if (format === "date-time" || property.drizzle?.mode === "timestamp") drizzleType = `integer({ mode: 'timestamp' })`;
|
|
1142
|
-
else if (format === "json" || property.drizzle?.mode === "json") drizzleType = `text({ mode: 'json' })`;
|
|
1143
|
-
else drizzleType = "text()";
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
case "integer": {
|
|
1147
|
-
const mode = property.drizzle?.mode;
|
|
1148
|
-
if (mode === "boolean") drizzleType = `integer({ mode: 'boolean' })`;
|
|
1149
|
-
else if (mode === "timestamp" || mode === "timestamp_ms") drizzleType = `integer({ mode: '${mode}' })`;
|
|
1150
|
-
else if (mode === "bigint") drizzleType = `integer({ mode: 'bigint' })`;
|
|
1151
|
-
else drizzleType = "integer()";
|
|
1152
|
-
break;
|
|
1153
|
-
}
|
|
1154
|
-
case "number":
|
|
1155
|
-
drizzleType = "real()";
|
|
1156
|
-
break;
|
|
1157
|
-
case "boolean":
|
|
1158
|
-
drizzleType = `integer({ mode: 'boolean' })`;
|
|
1159
|
-
break;
|
|
1160
|
-
case "object":
|
|
1161
|
-
case "array":
|
|
1162
|
-
drizzleType = `text({ mode: 'json' })`;
|
|
1163
|
-
break;
|
|
1164
|
-
case "null":
|
|
1165
|
-
drizzleType = "text()";
|
|
1166
|
-
break;
|
|
1167
|
-
default: drizzleType = "text()";
|
|
1168
|
-
}
|
|
1169
|
-
return {
|
|
1170
|
-
name: snakeName,
|
|
1171
|
-
drizzleType,
|
|
1172
|
-
isPrimary,
|
|
1173
|
-
isAutoIncrement,
|
|
1174
|
-
isNullable: !isRequired && !isPrimary,
|
|
1175
|
-
isUnique: property.unique ?? false,
|
|
1176
|
-
defaultValue: property.default !== void 0 ? JSON.stringify(property.default) : void 0,
|
|
1177
|
-
isForeignKey: property.foreignKey !== void 0,
|
|
1178
|
-
foreignKeyRef: property.foreignKey ?? void 0
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function parseObjectToTable(schema, _warnings) {
|
|
1182
|
-
const tableName = schema.table.name;
|
|
1183
|
-
const columns = [];
|
|
1184
|
-
const requiredFields = new Set(schema.required ?? []);
|
|
1185
|
-
const autoColumns = /* @__PURE__ */ new Set();
|
|
1186
|
-
if (schema.table.timestamps) {
|
|
1187
|
-
autoColumns.add("created_at");
|
|
1188
|
-
autoColumns.add("updated_at");
|
|
1189
|
-
}
|
|
1190
|
-
if (schema.table.softDelete) autoColumns.add("deleted_at");
|
|
1191
|
-
for (const [propName, prop] of Object.entries(schema.properties)) {
|
|
1192
|
-
if (prop.nested?.enabled && (prop.type === "object" || prop.type === "array")) continue;
|
|
1193
|
-
if (prop.type === "array" && prop.items?.nested?.enabled) continue;
|
|
1194
|
-
const snakeName = toSnakeCase(propName);
|
|
1195
|
-
if (autoColumns.has(snakeName)) continue;
|
|
1196
|
-
const column = mapPropertyToColumn(propName, prop, requiredFields.has(propName));
|
|
1197
|
-
columns.push(column);
|
|
1198
|
-
}
|
|
1199
|
-
if (schema.table.timestamps) {
|
|
1200
|
-
columns.push({
|
|
1201
|
-
name: "created_at",
|
|
1202
|
-
drizzleType: `integer({ mode: 'timestamp' })`,
|
|
1203
|
-
isPrimary: false,
|
|
1204
|
-
isAutoIncrement: false,
|
|
1205
|
-
isNullable: false,
|
|
1206
|
-
isUnique: false,
|
|
1207
|
-
defaultValue: void 0
|
|
1208
|
-
});
|
|
1209
|
-
columns.push({
|
|
1210
|
-
name: "updated_at",
|
|
1211
|
-
drizzleType: `integer({ mode: 'timestamp' })`,
|
|
1212
|
-
isPrimary: false,
|
|
1213
|
-
isAutoIncrement: false,
|
|
1214
|
-
isNullable: false,
|
|
1215
|
-
isUnique: false,
|
|
1216
|
-
defaultValue: void 0
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
if (schema.table.softDelete) columns.push({
|
|
1220
|
-
name: "deleted_at",
|
|
1221
|
-
drizzleType: `integer({ mode: 'timestamp' })`,
|
|
1222
|
-
isPrimary: false,
|
|
1223
|
-
isAutoIncrement: false,
|
|
1224
|
-
isNullable: true,
|
|
1225
|
-
isUnique: false,
|
|
1226
|
-
defaultValue: void 0
|
|
1227
|
-
});
|
|
1228
|
-
return {
|
|
1229
|
-
name: tableName,
|
|
1230
|
-
columns
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
function parseNestedObject(propName, property, parentTableName, warnings) {
|
|
1234
|
-
const nestedTableName = `${parentTableName}_${toSnakeCase(propName)}`;
|
|
1235
|
-
const columns = [];
|
|
1236
|
-
const relationType = property.nested?.relation === "has-many" ? "has-many" : "has-one";
|
|
1237
|
-
columns.push({
|
|
1238
|
-
name: "id",
|
|
1239
|
-
drizzleType: "integer()",
|
|
1240
|
-
isPrimary: true,
|
|
1241
|
-
isAutoIncrement: true,
|
|
1242
|
-
isNullable: false,
|
|
1243
|
-
isUnique: false,
|
|
1244
|
-
defaultValue: void 0
|
|
1245
|
-
});
|
|
1246
|
-
columns.push({
|
|
1247
|
-
name: `${parentTableName}_id`,
|
|
1248
|
-
drizzleType: "integer()",
|
|
1249
|
-
isPrimary: false,
|
|
1250
|
-
isAutoIncrement: false,
|
|
1251
|
-
isNullable: false,
|
|
1252
|
-
isUnique: false,
|
|
1253
|
-
defaultValue: void 0,
|
|
1254
|
-
isForeignKey: true,
|
|
1255
|
-
foreignKeyRef: {
|
|
1256
|
-
table: parentTableName,
|
|
1257
|
-
column: "id"
|
|
1258
|
-
}
|
|
1259
|
-
});
|
|
1260
|
-
if (property.type === "object" && property.properties) for (const [childName, childProp] of Object.entries(property.properties)) {
|
|
1261
|
-
if (childProp.nested?.enabled) {
|
|
1262
|
-
warnings.push(`Nested property "${childName}" inside "${nestedTableName}" is skipped — only one level of nesting is supported. Remove nested.enabled or use drizzle.mode: 'json' instead.`);
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
const column = mapPropertyToColumn(childName, childProp, false);
|
|
1266
|
-
columns.push(column);
|
|
1300
|
+
function parseOcrLanguages(languages) {
|
|
1301
|
+
return (languages ?? DEFAULT_OCR_LANGUAGES).split(",").map((language) => language.trim()).filter(Boolean);
|
|
1302
|
+
}
|
|
1303
|
+
async function recognizeImageText(imagePath, config, runtime = defaultRuntime) {
|
|
1304
|
+
if (!isLocalOcrPlatform(runtime.platform)) throw new Error(t("errors.ocr.platformUnsupported", { platform: runtime.platform }));
|
|
1305
|
+
let localOcr;
|
|
1306
|
+
try {
|
|
1307
|
+
localOcr = await runtime.loadLocalOcr();
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1310
|
+
throw new Error(t("errors.ocr.unavailable", { error: message }));
|
|
1267
1311
|
}
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
fromTable: parentTableName,
|
|
1278
|
-
toTable: nestedTableName,
|
|
1279
|
-
name: toSnakeCase(propName)
|
|
1280
|
-
};
|
|
1312
|
+
const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, parseOcrLanguages(config?.ocrLanguages));
|
|
1313
|
+
const text = result.text.trim();
|
|
1314
|
+
if (!text) throw new Error(t("errors.ocr.noText"));
|
|
1315
|
+
const confidence = result.confidence;
|
|
1316
|
+
const minConfidence = config?.ocrMinConfidence ?? 0;
|
|
1317
|
+
if (confidence < minConfidence) throw new Error(t("errors.ocr.lowConfidence", {
|
|
1318
|
+
confidence: (confidence * 100).toFixed(1),
|
|
1319
|
+
min: (minConfidence * 100).toFixed(1)
|
|
1320
|
+
}));
|
|
1281
1321
|
return {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
columns
|
|
1285
|
-
},
|
|
1286
|
-
relation,
|
|
1287
|
-
reverseRelation
|
|
1322
|
+
text,
|
|
1323
|
+
confidence
|
|
1288
1324
|
};
|
|
1289
1325
|
}
|
|
1290
|
-
function
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1326
|
+
function normalizeOcrText(text) {
|
|
1327
|
+
return text.replace(/\s+/g, "").trim().toUpperCase();
|
|
1328
|
+
}
|
|
1329
|
+
async function checkImageOcrAvailability(imagePath, runtime = defaultRuntime) {
|
|
1330
|
+
if (!isLocalOcrPlatform(runtime.platform)) return {
|
|
1331
|
+
platformSupported: false,
|
|
1332
|
+
dependencyLoaded: false,
|
|
1333
|
+
ocrOk: null,
|
|
1334
|
+
imagePath,
|
|
1335
|
+
error: t("errors.ocr.platformUnsupported", { platform: runtime.platform })
|
|
1336
|
+
};
|
|
1337
|
+
let localOcr;
|
|
1338
|
+
try {
|
|
1339
|
+
localOcr = await runtime.loadLocalOcr();
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
return {
|
|
1342
|
+
platformSupported: true,
|
|
1343
|
+
dependencyLoaded: false,
|
|
1344
|
+
ocrOk: null,
|
|
1345
|
+
imagePath,
|
|
1346
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1347
|
+
};
|
|
1307
1348
|
}
|
|
1308
|
-
return {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1349
|
+
if (!imagePath) return {
|
|
1350
|
+
platformSupported: true,
|
|
1351
|
+
dependencyLoaded: true,
|
|
1352
|
+
ocrOk: null,
|
|
1353
|
+
error: "No OCR self-check image was found."
|
|
1313
1354
|
};
|
|
1355
|
+
try {
|
|
1356
|
+
const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, ["en-US"]);
|
|
1357
|
+
const recognizedText = result.text.trim();
|
|
1358
|
+
const ocrOk = normalizeOcrText(recognizedText).includes(SELF_CHECK_EXPECTED_TEXT);
|
|
1359
|
+
return {
|
|
1360
|
+
platformSupported: true,
|
|
1361
|
+
dependencyLoaded: true,
|
|
1362
|
+
ocrOk,
|
|
1363
|
+
imagePath,
|
|
1364
|
+
recognizedText,
|
|
1365
|
+
confidence: result.confidence,
|
|
1366
|
+
error: ocrOk ? void 0 : `Expected OCR text "${SELF_CHECK_EXPECTED_TEXT}" was not recognized.`
|
|
1367
|
+
};
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
return {
|
|
1370
|
+
platformSupported: true,
|
|
1371
|
+
dependencyLoaded: true,
|
|
1372
|
+
ocrOk: false,
|
|
1373
|
+
imagePath,
|
|
1374
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1314
1377
|
}
|
|
1315
1378
|
|
|
1316
1379
|
//#endregion
|
|
1317
|
-
//#region src/
|
|
1318
|
-
const DrizzleModeSchema = z.enum([
|
|
1319
|
-
"json",
|
|
1320
|
-
"timestamp",
|
|
1321
|
-
"timestamp_ms",
|
|
1322
|
-
"boolean",
|
|
1323
|
-
"bigint"
|
|
1324
|
-
]);
|
|
1325
|
-
const DrizzleExtensionSchema = z.object({
|
|
1326
|
-
mode: DrizzleModeSchema.optional(),
|
|
1327
|
-
customType: z.string().optional()
|
|
1328
|
-
}).optional();
|
|
1329
|
-
const NestedConfigSchema = z.object({
|
|
1330
|
-
enabled: z.literal(true),
|
|
1331
|
-
relation: z.enum(["has-one", "has-many"])
|
|
1332
|
-
});
|
|
1333
|
-
const ForeignKeyRefSchema = z.object({
|
|
1334
|
-
table: z.string().min(1),
|
|
1335
|
-
column: z.string().min(1)
|
|
1336
|
-
});
|
|
1337
|
-
const JsonSchemaPropertySchema = z.lazy(() => z.object({
|
|
1338
|
-
description: z.string().optional(),
|
|
1339
|
-
type: z.enum([
|
|
1340
|
-
"string",
|
|
1341
|
-
"integer",
|
|
1342
|
-
"number",
|
|
1343
|
-
"boolean",
|
|
1344
|
-
"object",
|
|
1345
|
-
"array",
|
|
1346
|
-
"null"
|
|
1347
|
-
]),
|
|
1348
|
-
format: z.string().optional(),
|
|
1349
|
-
primary: z.boolean().optional(),
|
|
1350
|
-
autoIncrement: z.boolean().optional(),
|
|
1351
|
-
unique: z.boolean().optional(),
|
|
1352
|
-
default: z.unknown().optional(),
|
|
1353
|
-
maxLength: z.number().int().positive().optional(),
|
|
1354
|
-
minLength: z.number().int().nonnegative().optional(),
|
|
1355
|
-
minimum: z.number().optional(),
|
|
1356
|
-
maximum: z.number().optional(),
|
|
1357
|
-
drizzle: DrizzleExtensionSchema,
|
|
1358
|
-
nested: NestedConfigSchema.optional(),
|
|
1359
|
-
foreignKey: ForeignKeyRefSchema.optional(),
|
|
1360
|
-
properties: z.record(z.string(), JsonSchemaPropertySchema).optional(),
|
|
1361
|
-
items: JsonSchemaPropertySchema.optional(),
|
|
1362
|
-
required: z.array(z.string()).optional()
|
|
1363
|
-
}));
|
|
1364
|
-
const TableConfigSchema = z.object({
|
|
1365
|
-
name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "Table name must be snake_case (lowercase letters, digits, underscores)"),
|
|
1366
|
-
timestamps: z.boolean().optional(),
|
|
1367
|
-
softDelete: z.boolean().optional()
|
|
1368
|
-
});
|
|
1369
|
-
const ExamplePairSchema = z.object({
|
|
1370
|
-
text: z.string().min(1),
|
|
1371
|
-
output: z.record(z.string(), z.unknown())
|
|
1372
|
-
});
|
|
1373
|
-
const JsonSchemaDefinitionSchema = z.object({
|
|
1374
|
-
$schema: z.string().optional(),
|
|
1375
|
-
title: z.string().min(1),
|
|
1376
|
-
description: z.string().optional(),
|
|
1377
|
-
type: z.literal("object"),
|
|
1378
|
-
table: TableConfigSchema,
|
|
1379
|
-
properties: z.record(z.string(), JsonSchemaPropertySchema),
|
|
1380
|
-
required: z.array(z.string()).optional(),
|
|
1381
|
-
examples: z.array(ExamplePairSchema).optional()
|
|
1382
|
-
}).refine((schema) => Object.keys(schema.properties).length >= 1, {
|
|
1383
|
-
message: "At least one property is required",
|
|
1384
|
-
path: ["properties"]
|
|
1385
|
-
}).refine((schema) => !schema.required || schema.required.every((r) => r in schema.properties), {
|
|
1386
|
-
message: "All required fields must be defined in properties",
|
|
1387
|
-
path: ["required"]
|
|
1388
|
-
}).refine((schema) => {
|
|
1389
|
-
return !Object.values(schema.properties).some((p) => p.primary) || Object.values(schema.properties).filter((p) => p.primary).length === 1;
|
|
1390
|
-
}, {
|
|
1391
|
-
message: "Only one primary key is allowed per table",
|
|
1392
|
-
path: ["properties"]
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
//#endregion
|
|
1396
|
-
//#region src/core/schema-sqlite/migrator.ts
|
|
1380
|
+
//#region src/infrastructure/schema/migration-config.ts
|
|
1397
1381
|
function createMigrationConfig(cwd) {
|
|
1398
1382
|
return {
|
|
1399
1383
|
schemaPath: `${cwd}/.aiex/schema`,
|
|
@@ -1416,7 +1400,7 @@ function generateDrizzleConfig() {
|
|
|
1416
1400
|
}
|
|
1417
1401
|
|
|
1418
1402
|
//#endregion
|
|
1419
|
-
//#region src/
|
|
1403
|
+
//#region src/application/doctor/collect-diagnostics.ts
|
|
1420
1404
|
const V1_SUFFIX_RE = /\/v1\/?$/;
|
|
1421
1405
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
1422
1406
|
async function checkConnection(baseURL) {
|
|
@@ -1593,4 +1577,98 @@ async function collectDoctorDiagnostics(options = {}) {
|
|
|
1593
1577
|
}
|
|
1594
1578
|
|
|
1595
1579
|
//#endregion
|
|
1596
|
-
|
|
1580
|
+
//#region src/infrastructure/schema/generate-drizzle-schema.ts
|
|
1581
|
+
function renderColumnType(ct) {
|
|
1582
|
+
switch (ct.class) {
|
|
1583
|
+
case "text": return ct.mode === "json" ? `text({ mode: 'json' })` : "text()";
|
|
1584
|
+
case "integer": return ct.mode ? `integer({ mode: '${ct.mode}' })` : "integer()";
|
|
1585
|
+
case "real": return "real()";
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function renderDefaultValue(value) {
|
|
1589
|
+
return JSON.stringify(value);
|
|
1590
|
+
}
|
|
1591
|
+
function generateColumnDefinition(column) {
|
|
1592
|
+
if (column.isPrimary && column.isAutoIncrement) return ` ${column.name}: integer().primaryKey({ autoIncrement: true })`;
|
|
1593
|
+
let def = ` ${column.name}: ${renderColumnType(column.columnType)}`;
|
|
1594
|
+
if (column.isPrimary) def += ".primaryKey()";
|
|
1595
|
+
if (!column.isNullable && !column.isPrimary) def += ".notNull()";
|
|
1596
|
+
if (column.isUnique && !column.isPrimary) def += ".unique()";
|
|
1597
|
+
if (column.default !== void 0) def += `.default(${renderDefaultValue(column.default)})`;
|
|
1598
|
+
if (column.isForeignKey && column.foreignKeyRef) def += `.references(() => ${column.foreignKeyRef.table}.${column.foreignKeyRef.column})`;
|
|
1599
|
+
return def;
|
|
1600
|
+
}
|
|
1601
|
+
function renderCheckToDrizzle(check, tableVar) {
|
|
1602
|
+
const colRef = `\${${tableVar}.${check.column}}`;
|
|
1603
|
+
let expr;
|
|
1604
|
+
switch (check.kind) {
|
|
1605
|
+
case "min_length":
|
|
1606
|
+
expr = `length(${colRef}) >= ${check.value}`;
|
|
1607
|
+
break;
|
|
1608
|
+
case "max_length":
|
|
1609
|
+
expr = `length(${colRef}) <= ${check.value}`;
|
|
1610
|
+
break;
|
|
1611
|
+
case "min_value":
|
|
1612
|
+
expr = `${colRef} >= ${check.value}`;
|
|
1613
|
+
break;
|
|
1614
|
+
case "max_value":
|
|
1615
|
+
expr = `${colRef} <= ${check.value}`;
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
return ` ${check.name}: check('${check.name}', sql\`${expr}\`)`;
|
|
1619
|
+
}
|
|
1620
|
+
function generateTableDefinition(table) {
|
|
1621
|
+
const columns = table.columns.map(generateColumnDefinition);
|
|
1622
|
+
if (!table.checks?.length) return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n})`;
|
|
1623
|
+
const checkLines = table.checks.map((c) => renderCheckToDrizzle(c, "table"));
|
|
1624
|
+
return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n}, (table) => ({\n${checkLines.join(",\n")}\n}))`;
|
|
1625
|
+
}
|
|
1626
|
+
function generateRelationDefinitions(relations, reverseRelations) {
|
|
1627
|
+
if (relations.length === 0 && reverseRelations.length === 0) return "";
|
|
1628
|
+
const definitions = [];
|
|
1629
|
+
const childByTable = /* @__PURE__ */ new Map();
|
|
1630
|
+
for (const rel of relations) {
|
|
1631
|
+
const list = childByTable.get(rel.fromTable) ?? [];
|
|
1632
|
+
list.push(rel);
|
|
1633
|
+
childByTable.set(rel.fromTable, list);
|
|
1634
|
+
}
|
|
1635
|
+
const parentByTable = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const rel of reverseRelations) {
|
|
1637
|
+
const list = parentByTable.get(rel.fromTable) ?? [];
|
|
1638
|
+
list.push(rel);
|
|
1639
|
+
parentByTable.set(rel.fromTable, list);
|
|
1640
|
+
}
|
|
1641
|
+
const allTableNames = new Set([...childByTable.keys(), ...parentByTable.keys()]);
|
|
1642
|
+
for (const tableName of allTableNames) {
|
|
1643
|
+
const childRels = childByTable.get(tableName) ?? [];
|
|
1644
|
+
const parentRels = parentByTable.get(tableName) ?? [];
|
|
1645
|
+
const needsOne = childRels.length > 0 || parentRels.some((r) => r.type === "has-one");
|
|
1646
|
+
const needsMany = parentRels.some((r) => r.type === "has-many");
|
|
1647
|
+
const imports = [];
|
|
1648
|
+
if (needsOne) imports.push("one");
|
|
1649
|
+
if (needsMany) imports.push("many");
|
|
1650
|
+
const importStr = imports.join(", ");
|
|
1651
|
+
const relDefs = [];
|
|
1652
|
+
for (const rel of childRels) relDefs.push(` ${rel.name}: one(${rel.toTable}, {\n fields: [${rel.fromTable}.${rel.fromColumn}],\n references: [${rel.toTable}.${rel.toColumn}],\n })`);
|
|
1653
|
+
for (const rel of parentRels) if (rel.type === "has-many") relDefs.push(` ${rel.name}: many(${rel.toTable})`);
|
|
1654
|
+
else relDefs.push(` ${rel.name}: one(${rel.toTable})`);
|
|
1655
|
+
definitions.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${importStr} }) => ({\n${relDefs.join(",\n")}\n}))`);
|
|
1656
|
+
}
|
|
1657
|
+
return definitions.join("\n\n");
|
|
1658
|
+
}
|
|
1659
|
+
function generateDrizzleSchema(result) {
|
|
1660
|
+
const imports = `import { ${`sqliteTable, text, integer, real${result.tables.some((t$1) => t$1.checks?.length) ? ", check, sql" : ""}`} } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'`;
|
|
1661
|
+
const tableDefs = result.tables.map(generateTableDefinition).join("\n\n");
|
|
1662
|
+
const relationDefs = generateRelationDefinitions(result.relations, result.reverseRelations);
|
|
1663
|
+
const parts = [
|
|
1664
|
+
imports,
|
|
1665
|
+
"",
|
|
1666
|
+
tableDefs
|
|
1667
|
+
];
|
|
1668
|
+
if (relationDefs) parts.push("", relationDefs);
|
|
1669
|
+
parts.push("");
|
|
1670
|
+
return parts.join("\n");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
//#endregion
|
|
1674
|
+
export { PLACEHOLDER_TEXT as C, name as D, description as E, package_default as O, PLACEHOLDER_SCHEMA as S, seedConfig as T, doctorDiagnosticsTableRows as _, recognizeImageText as a, DEFAULT_MINERU_CONFIG as b, t as c, writeAIConfig as d, AIConfigSchema as f, buildDoctorDiagnostics as g, toSnakeCase as h, generateDrizzleConfig as i, version as k, getDefaultAIConfig as l, parseJsonSchema as m, collectDoctorDiagnostics as n, shouldUseImageOcrFallback as o, JsonSchemaDefinitionSchema as p, createMigrationConfig as r, initI18n as s, generateDrizzleSchema as t, readAIConfig as u, formatDoctorDiagnosticsJson as v, createConfig as w, DEFAULT_PROMPT_CONFIG as x, DEFAULT_MINERU_API_CONFIG as y };
|