aiex-cli 0.1.1-beta.1 → 0.1.1-beta.3
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 +3 -2
- package/dist/cli.mjs +262 -30
- package/dist/{generate-drizzle-schema-CaSMqQWx.mjs → generate-drizzle-schema-DbJcRtaQ.mjs} +138 -27
- package/dist/index.d.mts +19 -4
- package/dist/index.mjs +1 -1
- package/dist/table-schema.json +35 -11
- package/dist/web/assets/{AISettings-CxoghpZX.js → AISettings-CRumcTjo.js} +10 -10
- package/dist/web/assets/{DataBrowser-BqmnFDWC.js → DataBrowser-BqRaZJrX.js} +2 -2
- package/dist/web/assets/{ExtractionViewer-Bs8c6xa2.js → ExtractionViewer-lbdQGv2F.js} +1 -1
- package/dist/web/assets/{JsonSchemaEditor-ITVm2zG1.js → JsonSchemaEditor-tjxtfHIF.js} +178 -179
- package/dist/web/assets/api-client-CpqFbcyH.js +1 -0
- package/dist/web/assets/index-BdQzcBMn.css +2 -0
- package/dist/web/assets/{index-DhL7jaO_.js → index-CivTtAYN.js} +8 -8
- package/dist/web/assets/object-utils-BahkewF-.js +1 -0
- package/dist/web/index.html +4 -4
- package/dist/{zh-CN-Cs4MViVi.mjs → zh-CN-BAGJklRp.mjs} +7 -3
- package/package.json +1 -1
- package/dist/web/assets/api-client-EJKabzZK.js +0 -1
- package/dist/web/assets/index-D0So2rJE.css +0 -2
- package/dist/web/assets/object-utils-CqCiBqJ4.js +0 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ aiex watch -s invoice -d ./watch_folder # watch folder daemon for automatic extr
|
|
|
32
32
|
|
|
33
33
|
## ✨ Features
|
|
34
34
|
|
|
35
|
-
- **JSON Schema → SQLite** — Define tables
|
|
35
|
+
- **AIEX JSON Schema → SQLite** — Define tables with a Drizzle-backed JSON Schema dialect, generate Drizzle ORM schema, and migrate to SQLite
|
|
36
36
|
- **Web Configuration & Viewer** — Browser-based UI for designing schemas, configuring integrations, previewing prompts, and browsing extracted data
|
|
37
37
|
- **AI Extraction** — Extract structured data from files (text, images, PDFs) using any OpenAI-compatible provider (OpenAI, Anthropic, Ollama, DeepSeek, local models, etc.)
|
|
38
38
|
- **Interactive Mode** — Run `aiex extract` without arguments for a guided extraction workflow
|
|
@@ -61,7 +61,7 @@ Opens a browser UI where you can visually design and manage your schemas, config
|
|
|
61
61
|
aiex schema
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Converts
|
|
64
|
+
Converts AIEX JSON Schema files into a SQLite database with full migration support. AIEX uses a Drizzle-backed schema dialect rather than the full JSON Schema specification; see [Docs/schema-dialect.md](Docs/schema-dialect.md) for the supported mapping surface.
|
|
65
65
|
|
|
66
66
|
### 3. Extract Data
|
|
67
67
|
|
|
@@ -107,6 +107,7 @@ Runs a background watcher daemon to monitor a folder for new incoming files (suc
|
|
|
107
107
|
| --- | --- |
|
|
108
108
|
| `aiex schema` | Parse JSON Schema files and migrate to SQLite |
|
|
109
109
|
| `aiex schema --generate` | Generate Drizzle schema code only (skip migration) |
|
|
110
|
+
| `aiex schema --force` | Allow a high-risk schema migration after reviewing the migration risk report |
|
|
110
111
|
| `aiex web` | Launch visual schema/configuration UI and data viewer in browser |
|
|
111
112
|
| `aiex extract` | Interactive mode — prompts for schema and file/directory input |
|
|
112
113
|
| `aiex extract -s <name> -f <file>` | Extract structured data from a file and insert into SQLite database |
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as package_default, C as DEFAULT_PROMPT_CONFIG, D as seedConfig, E as createConfig, O as description, S as DEFAULT_MINERU_CONFIG, T as PLACEHOLDER_TEXT, _ as doctorDiagnosticsSeverityRows, a as recognizeImageText, b as DEFAULT_LITEPARSE_CONFIG, c as t, d as writeAIConfig, f as AIConfigSchema, h as toSnakeCase, j as version, k as name, l as getDefaultAIConfig, m as parseJsonSchema, n as collectDoctorDiagnostics, o as shouldUseImageOcrFallback, p as JsonSchemaDefinitionSchema, r as createMigrationConfig, s as initI18n, t as generateDrizzleSchema, u as readAIConfig, v as doctorDiagnosticsTableRows, w as PLACEHOLDER_SCHEMA, x as DEFAULT_MINERU_API_CONFIG, y as formatDoctorDiagnosticsJson } from "./generate-drizzle-schema-
|
|
1
|
+
import { A as package_default, C as DEFAULT_PROMPT_CONFIG, D as seedConfig, E as createConfig, O as description, S as DEFAULT_MINERU_CONFIG, T as PLACEHOLDER_TEXT, _ as doctorDiagnosticsSeverityRows, a as recognizeImageText, b as DEFAULT_LITEPARSE_CONFIG, c as t, d as writeAIConfig, f as AIConfigSchema, h as toSnakeCase, j as version, k as name, l as getDefaultAIConfig, m as parseJsonSchema, n as collectDoctorDiagnostics, o as shouldUseImageOcrFallback, p as JsonSchemaDefinitionSchema, r as createMigrationConfig, s as initI18n, t as generateDrizzleSchema, u as readAIConfig, v as doctorDiagnosticsTableRows, w as PLACEHOLDER_SCHEMA, x as DEFAULT_MINERU_API_CONFIG, y as formatDoctorDiagnosticsJson } from "./generate-drizzle-schema-DbJcRtaQ.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
@@ -1162,16 +1162,16 @@ function getFileHash(filePath) {
|
|
|
1162
1162
|
//#endregion
|
|
1163
1163
|
//#region src/domain/ai-extraction/evidence.ts
|
|
1164
1164
|
const NUMERIC_RE = /^-?\d+(?:\.\d+)?$/;
|
|
1165
|
-
function isRecord$
|
|
1165
|
+
function isRecord$3(value) {
|
|
1166
1166
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1167
1167
|
}
|
|
1168
1168
|
function stripEvidence(data) {
|
|
1169
|
-
if (!isRecord$
|
|
1169
|
+
if (!isRecord$3(data)) return { data };
|
|
1170
1170
|
const { _evidence, ...businessData } = data;
|
|
1171
|
-
if (!isRecord$
|
|
1171
|
+
if (!isRecord$3(_evidence)) return { data: businessData };
|
|
1172
1172
|
return {
|
|
1173
1173
|
data: businessData,
|
|
1174
|
-
rawEvidence: Object.fromEntries(Object.entries(_evidence).filter(([, value]) => isRecord$
|
|
1174
|
+
rawEvidence: Object.fromEntries(Object.entries(_evidence).filter(([, value]) => isRecord$3(value)).map(([field, value]) => [field, value]))
|
|
1175
1175
|
};
|
|
1176
1176
|
}
|
|
1177
1177
|
function findExactUnique(text$1, quote) {
|
|
@@ -1203,7 +1203,7 @@ function quoteContainsValue(quote, value, property) {
|
|
|
1203
1203
|
return false;
|
|
1204
1204
|
}
|
|
1205
1205
|
function verifyFieldEvidence(input) {
|
|
1206
|
-
if (!input.text || !isRecord$
|
|
1206
|
+
if (!input.text || !isRecord$3(input.data) || !input.rawEvidence) return void 0;
|
|
1207
1207
|
const verified = {};
|
|
1208
1208
|
for (const [field, raw] of Object.entries(input.rawEvidence)) {
|
|
1209
1209
|
const property = input.schema.properties[field];
|
|
@@ -1437,7 +1437,7 @@ function propertyToExtractionSchema(property) {
|
|
|
1437
1437
|
}
|
|
1438
1438
|
return { type: nullableType(property.type) };
|
|
1439
1439
|
}
|
|
1440
|
-
function isRecord$
|
|
1440
|
+
function isRecord$2(value) {
|
|
1441
1441
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1442
1442
|
}
|
|
1443
1443
|
function schemaToExtractionOutputSchema(schema) {
|
|
@@ -1475,7 +1475,7 @@ function validatePropertyValue(path$1, property, value, issues) {
|
|
|
1475
1475
|
}
|
|
1476
1476
|
return;
|
|
1477
1477
|
case "object":
|
|
1478
|
-
if (!isRecord$
|
|
1478
|
+
if (!isRecord$2(value)) {
|
|
1479
1479
|
issues.push(`${path$1}: expected object or null`);
|
|
1480
1480
|
return;
|
|
1481
1481
|
}
|
|
@@ -1498,7 +1498,7 @@ function validateProperties(basePath, properties, data, issues) {
|
|
|
1498
1498
|
}
|
|
1499
1499
|
}
|
|
1500
1500
|
function validateExtractedData(schema, data) {
|
|
1501
|
-
if (!isRecord$
|
|
1501
|
+
if (!isRecord$2(data)) return {
|
|
1502
1502
|
success: false,
|
|
1503
1503
|
error: "Extracted data must be a JSON object."
|
|
1504
1504
|
};
|
|
@@ -1519,11 +1519,11 @@ const EVIDENCE_INSTRUCTIONS = `Evidence requirements:
|
|
|
1519
1519
|
- The quote must be an exact contiguous substring copied from the input text.
|
|
1520
1520
|
- Do not invent offsets. Only provide quotes.
|
|
1521
1521
|
- If no exact quote supports a field, omit that field from "_evidence".`;
|
|
1522
|
-
function isRecord(value) {
|
|
1522
|
+
function isRecord$1(value) {
|
|
1523
1523
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1524
1524
|
}
|
|
1525
1525
|
function withEvidenceSchema(schema) {
|
|
1526
|
-
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1526
|
+
const properties = isRecord$1(schema.properties) ? schema.properties : {};
|
|
1527
1527
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
1528
1528
|
return {
|
|
1529
1529
|
...schema,
|
|
@@ -2748,6 +2748,73 @@ function cancel$1(msg) {
|
|
|
2748
2748
|
process.exitCode = 0;
|
|
2749
2749
|
}
|
|
2750
2750
|
|
|
2751
|
+
//#endregion
|
|
2752
|
+
//#region src/domain/schema/migration-risk.ts
|
|
2753
|
+
function keyOf(entry) {
|
|
2754
|
+
return `${entry.table}.${entry.column}`;
|
|
2755
|
+
}
|
|
2756
|
+
function maxSeverity(items) {
|
|
2757
|
+
if (items.some((item) => item.severity === "high")) return "high";
|
|
2758
|
+
if (items.some((item) => item.severity === "medium")) return "medium";
|
|
2759
|
+
if (items.some((item) => item.severity === "low")) return "low";
|
|
2760
|
+
return "none";
|
|
2761
|
+
}
|
|
2762
|
+
function addRisk(items, severity, kind, table, column, message) {
|
|
2763
|
+
items.push({
|
|
2764
|
+
severity,
|
|
2765
|
+
kind,
|
|
2766
|
+
table,
|
|
2767
|
+
column,
|
|
2768
|
+
message
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
function enumValues(entry) {
|
|
2772
|
+
return new Set((entry.constraints?.enumValues ?? []).map((value) => JSON.stringify(value)));
|
|
2773
|
+
}
|
|
2774
|
+
function isEnumNarrowed(previous, next) {
|
|
2775
|
+
const prev = enumValues(previous);
|
|
2776
|
+
const current = enumValues(next);
|
|
2777
|
+
if (prev.size === 0 || current.size === 0 || current.size >= prev.size) return false;
|
|
2778
|
+
return [...current].every((value) => prev.has(value));
|
|
2779
|
+
}
|
|
2780
|
+
function enumChanged(previous, next) {
|
|
2781
|
+
const prev = enumValues(previous);
|
|
2782
|
+
const current = enumValues(next);
|
|
2783
|
+
if (prev.size === 0 && current.size === 0) return false;
|
|
2784
|
+
if (prev.size !== current.size) return true;
|
|
2785
|
+
return [...prev].some((value) => !current.has(value));
|
|
2786
|
+
}
|
|
2787
|
+
function analyzeMigrationRisk(previousEntries, nextEntries) {
|
|
2788
|
+
const items = [];
|
|
2789
|
+
const previousByKey = new Map(previousEntries.map((entry) => [keyOf(entry), entry]));
|
|
2790
|
+
const nextByKey = new Map(nextEntries.map((entry) => [keyOf(entry), entry]));
|
|
2791
|
+
const previousTables = new Set(previousEntries.map((entry) => entry.table));
|
|
2792
|
+
const nextTables = new Set(nextEntries.map((entry) => entry.table));
|
|
2793
|
+
for (const table of previousTables) if (!nextTables.has(table)) addRisk(items, "high", "table_removed", table, void 0, `Table "${table}" will be removed.`);
|
|
2794
|
+
for (const table of nextTables) if (!previousTables.has(table)) addRisk(items, "low", "table_added", table, void 0, `Table "${table}" will be added.`);
|
|
2795
|
+
for (const [key, previous] of previousByKey) {
|
|
2796
|
+
const next = nextByKey.get(key);
|
|
2797
|
+
if (!next) {
|
|
2798
|
+
addRisk(items, "high", "column_removed", previous.table, previous.column, `Column "${key}" will be removed.`);
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
if (previous.sqliteType !== next.sqliteType || previous.drizzleType !== next.drizzleType) addRisk(items, "high", "column_type_changed", previous.table, previous.column, `Column "${key}" type changes from ${previous.drizzleType} to ${next.drizzleType}.`);
|
|
2802
|
+
if (previous.nullable && !next.nullable) addRisk(items, "high", "nullable_tightened", previous.table, previous.column, `Column "${key}" changes from nullable to not null.`);
|
|
2803
|
+
else if (!previous.nullable && next.nullable) addRisk(items, "medium", "nullable_relaxed", previous.table, previous.column, `Column "${key}" changes from not null to nullable.`);
|
|
2804
|
+
if (!previous.unique && next.unique) addRisk(items, "high", "unique_added", previous.table, previous.column, `Column "${key}" adds a unique constraint.`);
|
|
2805
|
+
if (previous.primary !== next.primary) addRisk(items, "high", "primary_changed", previous.table, previous.column, `Column "${key}" primary key status changes.`);
|
|
2806
|
+
if (isEnumNarrowed(previous, next)) addRisk(items, "high", "enum_narrowed", previous.table, previous.column, `Column "${key}" enum values are narrowed.`);
|
|
2807
|
+
else if (enumChanged(previous, next)) addRisk(items, "medium", "enum_changed", previous.table, previous.column, `Column "${key}" enum values change.`);
|
|
2808
|
+
}
|
|
2809
|
+
for (const [key, next] of nextByKey) if (!previousByKey.has(key)) addRisk(items, next.nullable ? "low" : "medium", "column_added", next.table, next.column, `Column "${key}" will be added.`);
|
|
2810
|
+
const level = maxSeverity(items);
|
|
2811
|
+
return {
|
|
2812
|
+
level,
|
|
2813
|
+
items,
|
|
2814
|
+
hasHighRisk: level === "high"
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2751
2818
|
//#endregion
|
|
2752
2819
|
//#region src/infrastructure/runtime/package-paths.ts
|
|
2753
2820
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -2772,6 +2839,102 @@ function resolveHelperPath() {
|
|
|
2772
2839
|
}
|
|
2773
2840
|
}
|
|
2774
2841
|
|
|
2842
|
+
//#endregion
|
|
2843
|
+
//#region src/domain/schema/dialect.ts
|
|
2844
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
2845
|
+
"$schema",
|
|
2846
|
+
"title",
|
|
2847
|
+
"description",
|
|
2848
|
+
"type",
|
|
2849
|
+
"table",
|
|
2850
|
+
"properties",
|
|
2851
|
+
"required",
|
|
2852
|
+
"examples"
|
|
2853
|
+
]);
|
|
2854
|
+
const PROPERTY_KEYS = new Set([
|
|
2855
|
+
"description",
|
|
2856
|
+
"type",
|
|
2857
|
+
"format",
|
|
2858
|
+
"pattern",
|
|
2859
|
+
"enum",
|
|
2860
|
+
"primary",
|
|
2861
|
+
"autoIncrement",
|
|
2862
|
+
"unique",
|
|
2863
|
+
"default",
|
|
2864
|
+
"maxLength",
|
|
2865
|
+
"minLength",
|
|
2866
|
+
"minimum",
|
|
2867
|
+
"maximum",
|
|
2868
|
+
"examples",
|
|
2869
|
+
"xPrompt",
|
|
2870
|
+
"drizzle",
|
|
2871
|
+
"nested",
|
|
2872
|
+
"foreignKey",
|
|
2873
|
+
"properties",
|
|
2874
|
+
"items",
|
|
2875
|
+
"required"
|
|
2876
|
+
]);
|
|
2877
|
+
const DRIZZLE_KEYS = new Set(["mode"]);
|
|
2878
|
+
const NESTED_KEYS = new Set(["enabled", "relation"]);
|
|
2879
|
+
const FOREIGN_KEY_KEYS = new Set(["table", "column"]);
|
|
2880
|
+
const TABLE_KEYS = new Set([
|
|
2881
|
+
"name",
|
|
2882
|
+
"timestamps",
|
|
2883
|
+
"softDelete"
|
|
2884
|
+
]);
|
|
2885
|
+
const UNSUPPORTED_JSON_SCHEMA_KEYWORDS = new Set([
|
|
2886
|
+
"oneOf",
|
|
2887
|
+
"anyOf",
|
|
2888
|
+
"allOf",
|
|
2889
|
+
"not",
|
|
2890
|
+
"if",
|
|
2891
|
+
"then",
|
|
2892
|
+
"else",
|
|
2893
|
+
"const",
|
|
2894
|
+
"contains",
|
|
2895
|
+
"prefixItems",
|
|
2896
|
+
"additionalItems",
|
|
2897
|
+
"additionalProperties",
|
|
2898
|
+
"patternProperties",
|
|
2899
|
+
"propertyNames",
|
|
2900
|
+
"dependentRequired",
|
|
2901
|
+
"dependentSchemas",
|
|
2902
|
+
"dependencies",
|
|
2903
|
+
"unevaluatedItems",
|
|
2904
|
+
"unevaluatedProperties",
|
|
2905
|
+
"multipleOf",
|
|
2906
|
+
"exclusiveMinimum",
|
|
2907
|
+
"exclusiveMaximum"
|
|
2908
|
+
]);
|
|
2909
|
+
function isRecord(value) {
|
|
2910
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2911
|
+
}
|
|
2912
|
+
function warnUnsupportedKey(warnings, path$1, key) {
|
|
2913
|
+
const reason = UNSUPPORTED_JSON_SCHEMA_KEYWORDS.has(key) ? "is not part of the AIEX Drizzle-backed schema dialect and cannot be mapped reliably" : "is not recognized by the AIEX Drizzle-backed schema dialect";
|
|
2914
|
+
warnings.push(`${path$1}.${key} ${reason}.`);
|
|
2915
|
+
}
|
|
2916
|
+
function inspectRecordKeys(warnings, value, path$1, allowedKeys) {
|
|
2917
|
+
if (!isRecord(value)) return;
|
|
2918
|
+
for (const key of Object.keys(value)) if (!allowedKeys.has(key)) warnUnsupportedKey(warnings, path$1, key);
|
|
2919
|
+
}
|
|
2920
|
+
function inspectProperty(warnings, property, path$1) {
|
|
2921
|
+
if (!isRecord(property)) return;
|
|
2922
|
+
inspectRecordKeys(warnings, property, path$1, PROPERTY_KEYS);
|
|
2923
|
+
inspectRecordKeys(warnings, property.drizzle, `${path$1}.drizzle`, DRIZZLE_KEYS);
|
|
2924
|
+
inspectRecordKeys(warnings, property.nested, `${path$1}.nested`, NESTED_KEYS);
|
|
2925
|
+
inspectRecordKeys(warnings, property.foreignKey, `${path$1}.foreignKey`, FOREIGN_KEY_KEYS);
|
|
2926
|
+
if (isRecord(property.properties)) for (const [name$1, child] of Object.entries(property.properties)) inspectProperty(warnings, child, `${path$1}.properties.${name$1}`);
|
|
2927
|
+
if (property.items) inspectProperty(warnings, property.items, `${path$1}.items`);
|
|
2928
|
+
}
|
|
2929
|
+
function collectDialectWarnings(schema, filePath) {
|
|
2930
|
+
if (!isRecord(schema)) return [];
|
|
2931
|
+
const warnings = [];
|
|
2932
|
+
inspectRecordKeys(warnings, schema, filePath, TOP_LEVEL_KEYS);
|
|
2933
|
+
inspectRecordKeys(warnings, schema.table, `${filePath}.table`, TABLE_KEYS);
|
|
2934
|
+
if (isRecord(schema.properties)) for (const [name$1, property] of Object.entries(schema.properties)) inspectProperty(warnings, property, `${filePath}.properties.${name$1}`);
|
|
2935
|
+
return warnings;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2775
2938
|
//#endregion
|
|
2776
2939
|
//#region src/application/schema/parse-all-schemas.ts
|
|
2777
2940
|
function formatZodError(error, filePath) {
|
|
@@ -2782,6 +2945,7 @@ function parseAllSchemas(entries) {
|
|
|
2782
2945
|
const relations = [];
|
|
2783
2946
|
const reverseRelations = [];
|
|
2784
2947
|
const warnings = [];
|
|
2948
|
+
const mapping = [];
|
|
2785
2949
|
for (const { filePath, content } of entries) {
|
|
2786
2950
|
let schema;
|
|
2787
2951
|
try {
|
|
@@ -2793,11 +2957,16 @@ function parseAllSchemas(entries) {
|
|
|
2793
2957
|
};
|
|
2794
2958
|
}
|
|
2795
2959
|
try {
|
|
2960
|
+
warnings.push(...collectDialectWarnings(schema, filePath));
|
|
2796
2961
|
const result = parseJsonSchema(JsonSchemaDefinitionSchema.parse(schema));
|
|
2797
2962
|
tables.push(...result.tables);
|
|
2798
2963
|
relations.push(...result.relations);
|
|
2799
2964
|
reverseRelations.push(...result.reverseRelations);
|
|
2800
2965
|
warnings.push(...result.warnings);
|
|
2966
|
+
mapping.push(...(result.mapping ?? []).map((entry) => ({
|
|
2967
|
+
...entry,
|
|
2968
|
+
schemaPath: `${filePath}${entry.schemaPath.slice(1)}`
|
|
2969
|
+
})));
|
|
2801
2970
|
} catch (e) {
|
|
2802
2971
|
if (e instanceof ZodError) return {
|
|
2803
2972
|
success: false,
|
|
@@ -2813,6 +2982,7 @@ function parseAllSchemas(entries) {
|
|
|
2813
2982
|
relations,
|
|
2814
2983
|
reverseRelations,
|
|
2815
2984
|
warnings,
|
|
2985
|
+
mapping,
|
|
2816
2986
|
drizzleCode: generateDrizzleSchema({
|
|
2817
2987
|
tables,
|
|
2818
2988
|
relations,
|
|
@@ -2826,6 +2996,24 @@ function parseAllSchemas(entries) {
|
|
|
2826
2996
|
//#endregion
|
|
2827
2997
|
//#region src/application/schema/schema-sync.ts
|
|
2828
2998
|
const execFileAsync = promisify(execFile);
|
|
2999
|
+
const NO_RISK_REPORT = {
|
|
3000
|
+
level: "none",
|
|
3001
|
+
items: [],
|
|
3002
|
+
hasHighRisk: false
|
|
3003
|
+
};
|
|
3004
|
+
function schemaMapPath(config) {
|
|
3005
|
+
return path.join(path.dirname(config.drizzleSchemaPath), "schema-map.json");
|
|
3006
|
+
}
|
|
3007
|
+
async function readPreviousSchemaMap(config) {
|
|
3008
|
+
try {
|
|
3009
|
+
const content = await fs.readFile(schemaMapPath(config), "utf-8");
|
|
3010
|
+
const parsed = JSON.parse(content);
|
|
3011
|
+
if (Array.isArray(parsed.baselineEntries)) return parsed.baselineEntries;
|
|
3012
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
3013
|
+
} catch {
|
|
3014
|
+
return [];
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
2829
3017
|
async function listSchemaFiles(schemaDir) {
|
|
2830
3018
|
try {
|
|
2831
3019
|
return (await fs.readdir(schemaDir)).filter((f) => f.endsWith(".json")).map((f) => path.join(schemaDir, f)).sort();
|
|
@@ -2833,7 +3021,8 @@ async function listSchemaFiles(schemaDir) {
|
|
|
2833
3021
|
return [];
|
|
2834
3022
|
}
|
|
2835
3023
|
}
|
|
2836
|
-
async function generateSchemaFromFiles(schemaFiles, config) {
|
|
3024
|
+
async function generateSchemaFromFiles(schemaFiles, config, options = {}) {
|
|
3025
|
+
const previousMapping = await readPreviousSchemaMap(config);
|
|
2837
3026
|
const result = parseAllSchemas(await Promise.all(schemaFiles.map(async (filePath) => {
|
|
2838
3027
|
return {
|
|
2839
3028
|
filePath,
|
|
@@ -2846,17 +3035,30 @@ async function generateSchemaFromFiles(schemaFiles, config) {
|
|
|
2846
3035
|
warnings: [],
|
|
2847
3036
|
schemaCount: schemaFiles.length,
|
|
2848
3037
|
tables: 0,
|
|
2849
|
-
relations: 0
|
|
3038
|
+
relations: 0,
|
|
3039
|
+
mappingEntries: 0,
|
|
3040
|
+
riskReport: NO_RISK_REPORT
|
|
2850
3041
|
};
|
|
2851
|
-
const { tables, relations, reverseRelations, warnings, drizzleCode } = result.data;
|
|
3042
|
+
const { tables, relations, reverseRelations, warnings, mapping, drizzleCode } = result.data;
|
|
3043
|
+
const riskReport = previousMapping.length > 0 ? analyzeMigrationRisk(previousMapping, mapping) : NO_RISK_REPORT;
|
|
2852
3044
|
await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
|
|
2853
3045
|
await fs.writeFile(config.drizzleSchemaPath, drizzleCode);
|
|
3046
|
+
await fs.writeFile(schemaMapPath(config), `${JSON.stringify({
|
|
3047
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3048
|
+
dialect: "aiex-drizzle-sqlite",
|
|
3049
|
+
entries: mapping,
|
|
3050
|
+
baselineEntries: riskReport.hasHighRisk && !options.force ? previousMapping : void 0,
|
|
3051
|
+
warnings,
|
|
3052
|
+
migrationRisk: riskReport
|
|
3053
|
+
}, null, 2)}\n`);
|
|
2854
3054
|
return {
|
|
2855
3055
|
success: true,
|
|
2856
3056
|
warnings,
|
|
2857
3057
|
schemaCount: schemaFiles.length,
|
|
2858
3058
|
tables: tables.length,
|
|
2859
|
-
relations: relations.length + reverseRelations.length
|
|
3059
|
+
relations: relations.length + reverseRelations.length,
|
|
3060
|
+
mappingEntries: mapping.length,
|
|
3061
|
+
riskReport
|
|
2860
3062
|
};
|
|
2861
3063
|
}
|
|
2862
3064
|
function parseMigrationOutput(stdout, stderr) {
|
|
@@ -2908,24 +3110,34 @@ async function runSchemaSync(config, options = {}) {
|
|
|
2908
3110
|
warnings: [],
|
|
2909
3111
|
schemaCount: 0,
|
|
2910
3112
|
tables: 0,
|
|
2911
|
-
relations: 0
|
|
3113
|
+
relations: 0,
|
|
3114
|
+
mappingEntries: 0,
|
|
3115
|
+
riskReport: NO_RISK_REPORT
|
|
2912
3116
|
};
|
|
2913
|
-
const generated = await generateSchemaFromFiles(schemaFiles, config);
|
|
3117
|
+
const generated = await generateSchemaFromFiles(schemaFiles, config, { force: options.force });
|
|
2914
3118
|
if (!generated.success) return {
|
|
2915
3119
|
success: false,
|
|
2916
3120
|
error: generated.error,
|
|
2917
3121
|
warnings: generated.warnings,
|
|
2918
3122
|
schemaCount: generated.schemaCount,
|
|
2919
3123
|
tables: generated.tables,
|
|
2920
|
-
relations: generated.relations
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
success: true,
|
|
2924
|
-
warnings: generated.warnings,
|
|
2925
|
-
schemaCount: generated.schemaCount,
|
|
2926
|
-
tables: generated.tables,
|
|
2927
|
-
relations: generated.relations
|
|
3124
|
+
relations: generated.relations,
|
|
3125
|
+
mappingEntries: generated.mappingEntries,
|
|
3126
|
+
riskReport: generated.riskReport
|
|
2928
3127
|
};
|
|
3128
|
+
if (options.generateOnly || generated.riskReport.hasHighRisk && !options.force) {
|
|
3129
|
+
const blockedByRisk = generated.riskReport.hasHighRisk && !options.force && !options.generateOnly;
|
|
3130
|
+
return {
|
|
3131
|
+
success: !blockedByRisk,
|
|
3132
|
+
error: blockedByRisk ? t("errors.schema.highRiskMigrationBlocked") : void 0,
|
|
3133
|
+
warnings: generated.warnings,
|
|
3134
|
+
schemaCount: generated.schemaCount,
|
|
3135
|
+
tables: generated.tables,
|
|
3136
|
+
relations: generated.relations,
|
|
3137
|
+
mappingEntries: generated.mappingEntries,
|
|
3138
|
+
riskReport: generated.riskReport
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
2929
3141
|
const migration = await runSchemaMigration(config, options.migrationName);
|
|
2930
3142
|
return {
|
|
2931
3143
|
success: migration.success,
|
|
@@ -2934,6 +3146,8 @@ async function runSchemaSync(config, options = {}) {
|
|
|
2934
3146
|
schemaCount: generated.schemaCount,
|
|
2935
3147
|
tables: generated.tables,
|
|
2936
3148
|
relations: generated.relations,
|
|
3149
|
+
mappingEntries: generated.mappingEntries,
|
|
3150
|
+
riskReport: generated.riskReport,
|
|
2937
3151
|
migration
|
|
2938
3152
|
};
|
|
2939
3153
|
}
|
|
@@ -2955,6 +3169,11 @@ const schemaCommand = defineCommand({
|
|
|
2955
3169
|
name: {
|
|
2956
3170
|
type: "string",
|
|
2957
3171
|
description: t("command.schema.args.name")
|
|
3172
|
+
},
|
|
3173
|
+
force: {
|
|
3174
|
+
type: "boolean",
|
|
3175
|
+
description: t("command.schema.args.force"),
|
|
3176
|
+
default: false
|
|
2958
3177
|
}
|
|
2959
3178
|
},
|
|
2960
3179
|
async run({ args }) {
|
|
@@ -2969,7 +3188,7 @@ const schemaCommand = defineCommand({
|
|
|
2969
3188
|
}
|
|
2970
3189
|
const s1 = spinner();
|
|
2971
3190
|
s1.start(t("command.schema.generating"));
|
|
2972
|
-
const generated = await generateSchemaFromFiles(schemaFiles, config);
|
|
3191
|
+
const generated = await generateSchemaFromFiles(schemaFiles, config, { force: args.force });
|
|
2973
3192
|
for (const warning of generated.warnings) consola.warn(warning);
|
|
2974
3193
|
if (generated.success) consola.success(t("command.schema.generated", {
|
|
2975
3194
|
path: pc.cyan(".aiex/drizzle/schema.ts"),
|
|
@@ -2981,10 +3200,21 @@ const schemaCommand = defineCommand({
|
|
|
2981
3200
|
failCommand(t("common.failed"));
|
|
2982
3201
|
return;
|
|
2983
3202
|
}
|
|
3203
|
+
if (generated.riskReport.items.length > 0) {
|
|
3204
|
+
consola.info(t("command.schema.riskSummary", {
|
|
3205
|
+
level: generated.riskReport.level,
|
|
3206
|
+
count: generated.riskReport.items.length
|
|
3207
|
+
}));
|
|
3208
|
+
for (const item of generated.riskReport.items) consola.warn(`[${item.severity}] ${item.message}`);
|
|
3209
|
+
}
|
|
2984
3210
|
if (args.generate) {
|
|
2985
3211
|
outro(t("command.schema.runWithoutGenerate"));
|
|
2986
3212
|
return;
|
|
2987
3213
|
}
|
|
3214
|
+
if (generated.riskReport.hasHighRisk && !args.force) {
|
|
3215
|
+
failCommand(t("command.schema.highRiskBlocked", { flag: pc.cyan("--force") }));
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
2988
3218
|
const s2 = spinner();
|
|
2989
3219
|
s2.start(t("command.schema.runningMigrations"));
|
|
2990
3220
|
const migration = await runSchemaMigration(config, args.name);
|
|
@@ -16648,12 +16878,13 @@ function schemaRoutes(config) {
|
|
|
16648
16878
|
app.post("/migrate", async (c) => {
|
|
16649
16879
|
try {
|
|
16650
16880
|
await ensureDir();
|
|
16651
|
-
const result = await runSchemaSync(config);
|
|
16881
|
+
const result = await runSchemaSync(config, { force: c.req.query("force") === "true" });
|
|
16652
16882
|
if (!result.success) {
|
|
16653
|
-
const status = result.schemaCount === 0 ? 400 : 500;
|
|
16883
|
+
const status = result.schemaCount === 0 ? 400 : result.riskReport.hasHighRisk ? 409 : 500;
|
|
16654
16884
|
return c.json({
|
|
16655
16885
|
success: false,
|
|
16656
|
-
error: result.error || t("server.migrationFailed")
|
|
16886
|
+
error: result.error || t("server.migrationFailed"),
|
|
16887
|
+
riskReport: result.riskReport
|
|
16657
16888
|
}, status);
|
|
16658
16889
|
}
|
|
16659
16890
|
return c.json({
|
|
@@ -16662,7 +16893,8 @@ function schemaRoutes(config) {
|
|
|
16662
16893
|
tag: result.migration?.tag,
|
|
16663
16894
|
tables: result.tables,
|
|
16664
16895
|
relations: result.relations,
|
|
16665
|
-
warnings: result.warnings
|
|
16896
|
+
warnings: result.warnings,
|
|
16897
|
+
riskReport: result.riskReport
|
|
16666
16898
|
});
|
|
16667
16899
|
} catch (error) {
|
|
16668
16900
|
return c.json({
|