sqlite-hub 2.0.0 → 2.1.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 +15 -10
- package/docs/MCP.md +36 -4
- package/docs/changelog.md +11 -0
- package/docs/guidelines/DESIGN.md +64 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/logs_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +11 -0
- package/frontend/js/app.js +238 -8
- package/frontend/js/components/badges.js +16 -0
- package/frontend/js/components/connectionCard.js +35 -0
- package/frontend/js/components/modal.js +116 -245
- package/frontend/js/components/queryHistoryDetail.js +25 -45
- package/frontend/js/components/rowEditorPanel.js +7 -60
- package/frontend/js/components/structureGraph.js +292 -13
- package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
- package/frontend/js/components/tableDesignerEditor.js +51 -16
- package/frontend/js/components/tableDesignerSidebar.js +9 -5
- package/frontend/js/store.js +551 -7
- package/frontend/js/utils/connectionRegistry.js +76 -0
- package/frontend/js/utils/tableDesigner.js +431 -62
- package/frontend/js/views/backups.js +2 -5
- package/frontend/js/views/charts.js +20 -10
- package/frontend/js/views/connections.js +103 -3
- package/frontend/js/views/data.js +19 -5
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +28 -5
- package/frontend/js/views/tableAdvisor.js +14 -6
- package/frontend/js/views/tableDesigner.js +44 -9
- package/frontend/styles/components.css +676 -79
- package/frontend/styles/structure-graph.css +49 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/package.json +1 -1
- package/server/mcp/httpRouter.js +95 -0
- package/server/routes/connections.js +12 -0
- package/server/routes/data.js +1 -1
- package/server/routes/settings.js +29 -7
- package/server/routes/tableDesigner.js +15 -1
- package/server/server.js +15 -0
- package/server/services/mcpToolService.js +28 -0
- package/server/services/sqlite/connectionManager.js +23 -2
- package/server/services/sqlite/dataBrowserService.js +21 -0
- package/server/services/sqlite/introspection.js +39 -1
- package/server/services/sqlite/structureService.js +66 -11
- package/server/services/sqlite/tableAdvisor.js +6 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
- package/server/services/sqlite/tableDesigner/sql.js +31 -2
- package/server/services/sqlite/tableDesigner/validation.js +69 -9
- package/server/services/sqlite/tableDesignerService.js +110 -5
- package/server/services/storage/appStateStore.js +303 -4
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
|
@@ -41,6 +41,16 @@ function normalizeIdentifierKey(value) {
|
|
|
41
41
|
return normalizeTrimmed(value).toLowerCase();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function normalizeImportFormat(value, { allowEmpty = false } = {}) {
|
|
45
|
+
const normalized = normalizeIdentifierKey(value);
|
|
46
|
+
|
|
47
|
+
if (["csv", "tsv", "json"].includes(normalized)) {
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return allowEmpty ? "" : "csv";
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
function normalizeDesignerType(value) {
|
|
45
55
|
return normalizeTrimmed(value).toUpperCase() || "TEXT";
|
|
46
56
|
}
|
|
@@ -61,6 +71,30 @@ function createColumnId() {
|
|
|
61
71
|
return `column_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
function normalizeImportedCellValue(value) {
|
|
75
|
+
if (value === null || value === undefined) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (["boolean", "number", "string"].includes(typeof value)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
} catch {
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeImportedRows(rows) {
|
|
91
|
+
return Array.isArray(rows)
|
|
92
|
+
? rows.map((row) =>
|
|
93
|
+
Array.isArray(row) ? row.map((cell) => normalizeImportedCellValue(cell)) : []
|
|
94
|
+
)
|
|
95
|
+
: [];
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
export function createEmptyTableDesignerColumn(seed = {}) {
|
|
65
99
|
const type = normalizeDesignerType(seed.type ?? "TEXT");
|
|
66
100
|
const defaultValue = normalizeText(seed.defaultValue ?? "");
|
|
@@ -96,6 +130,16 @@ export function createEmptyTableDesignerColumn(seed = {}) {
|
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
function normalizeDraft(rawDraft = {}) {
|
|
133
|
+
const importRows = normalizeImportedRows(rawDraft.importRows ?? rawDraft.importedCsvRows);
|
|
134
|
+
const importSourceFileName = normalizeText(
|
|
135
|
+
rawDraft.importSourceFileName ?? rawDraft.importedCsvFileName ?? ""
|
|
136
|
+
);
|
|
137
|
+
const importDelimiter = normalizeText(rawDraft.importDelimiter ?? rawDraft.importedCsvDelimiter ?? "");
|
|
138
|
+
const importFormat = normalizeImportFormat(
|
|
139
|
+
rawDraft.importFormat ?? (importSourceFileName ? "csv" : ""),
|
|
140
|
+
{ allowEmpty: true }
|
|
141
|
+
);
|
|
142
|
+
|
|
99
143
|
return {
|
|
100
144
|
mode: String(rawDraft.mode ?? "create").trim() === "edit" ? "edit" : "create",
|
|
101
145
|
originalTableName: normalizeText(rawDraft.originalTableName ?? ""),
|
|
@@ -132,6 +176,7 @@ function normalizeDraft(rawDraft = {}) {
|
|
|
132
176
|
id: normalizeText(constraint.id ?? `check:${index}`),
|
|
133
177
|
name: normalizeText(constraint.name ?? `CHECK ${index + 1}`),
|
|
134
178
|
originalName: normalizeText(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`),
|
|
179
|
+
deleted: normalizeBoolean(constraint.deleted),
|
|
135
180
|
columns: Array.isArray(constraint.columns)
|
|
136
181
|
? constraint.columns
|
|
137
182
|
.map((column) => ({
|
|
@@ -146,18 +191,30 @@ function normalizeDraft(rawDraft = {}) {
|
|
|
146
191
|
originalExpression: normalizeText(constraint.originalExpression ?? constraint.expression ?? ""),
|
|
147
192
|
editable: normalizeBoolean(constraint.editable),
|
|
148
193
|
preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
|
|
194
|
+
columnId: normalizeText(constraint.columnId ?? ""),
|
|
195
|
+
source: normalizeText(constraint.source ?? (constraint.originalExpression ? "detected" : "user")),
|
|
196
|
+
presetId: normalizeText(constraint.presetId ?? ""),
|
|
197
|
+
presetFields:
|
|
198
|
+
constraint.presetFields && typeof constraint.presetFields === "object"
|
|
199
|
+
? Object.fromEntries(
|
|
200
|
+
Object.entries(constraint.presetFields).map(([key, value]) => [
|
|
201
|
+
key,
|
|
202
|
+
normalizeText(value),
|
|
203
|
+
])
|
|
204
|
+
)
|
|
205
|
+
: {},
|
|
149
206
|
}))
|
|
150
207
|
: [],
|
|
151
208
|
designerVersion: Number(rawDraft.designerVersion) || 1,
|
|
152
209
|
schemaWarnings: Array.isArray(rawDraft.schemaWarnings) ? rawDraft.schemaWarnings : [],
|
|
153
210
|
fillImportedRows: normalizeBoolean(rawDraft.fillImportedRows),
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
importFormat,
|
|
212
|
+
importSourceFileName,
|
|
213
|
+
importDelimiter,
|
|
214
|
+
importRows,
|
|
215
|
+
importedCsvFileName: importSourceFileName,
|
|
216
|
+
importedCsvDelimiter: importDelimiter,
|
|
217
|
+
importedCsvRows: importRows,
|
|
161
218
|
};
|
|
162
219
|
}
|
|
163
220
|
|
|
@@ -421,13 +478,40 @@ function buildColumnDefinition(column) {
|
|
|
421
478
|
return parts.join(" ");
|
|
422
479
|
}
|
|
423
480
|
|
|
481
|
+
function normalizeCheckExpressionSql(expression) {
|
|
482
|
+
const normalized = normalizeTrimmed(expression);
|
|
483
|
+
|
|
484
|
+
if (!normalized) {
|
|
485
|
+
return "";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (/^CHECK\s*\(/i.test(normalized)) {
|
|
489
|
+
return normalized;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return `CHECK (${normalized})`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildCheckConstraintSql(constraint) {
|
|
496
|
+
if (constraint.deleted) {
|
|
497
|
+
return "";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return normalizeCheckExpressionSql(constraint.expression);
|
|
501
|
+
}
|
|
502
|
+
|
|
424
503
|
function buildCreateTableSql(draft) {
|
|
425
504
|
const columnSql = draft.columns
|
|
426
505
|
.filter((column) => !column.deleted)
|
|
427
|
-
.map((column) => ` ${buildColumnDefinition(column)}`)
|
|
506
|
+
.map((column) => ` ${buildColumnDefinition(column)}`);
|
|
507
|
+
const checkSql = (draft.checkConstraints ?? [])
|
|
508
|
+
.map(buildCheckConstraintSql)
|
|
509
|
+
.filter(Boolean)
|
|
510
|
+
.map((constraintSql) => ` ${constraintSql}`);
|
|
511
|
+
const definitionSql = [...columnSql, ...checkSql]
|
|
428
512
|
.join(",\n");
|
|
429
513
|
|
|
430
|
-
return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${
|
|
514
|
+
return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${definitionSql}\n);`;
|
|
431
515
|
}
|
|
432
516
|
|
|
433
517
|
function getUniqueConstraintExpression(constraint) {
|
|
@@ -807,20 +891,30 @@ function escapeSqlLiteral(value) {
|
|
|
807
891
|
}
|
|
808
892
|
|
|
809
893
|
function formatImportedCellValueForSql(column, value) {
|
|
810
|
-
|
|
811
|
-
|
|
894
|
+
if (value === null || value === undefined) {
|
|
895
|
+
return "NULL";
|
|
896
|
+
}
|
|
897
|
+
|
|
812
898
|
const type = normalizeDesignerType(column.type);
|
|
899
|
+
const normalizedValue =
|
|
900
|
+
typeof value === "object" ? normalizeImportedCellValue(value) : value;
|
|
901
|
+
const textValue = normalizeText(normalizedValue);
|
|
902
|
+
const trimmedValue = normalizeTrimmed(normalizedValue);
|
|
813
903
|
|
|
814
904
|
if (!trimmedValue) {
|
|
815
905
|
return type === "TEXT" || type === "DATE" || type === "DATETIME" ? "''" : "NULL";
|
|
816
906
|
}
|
|
817
907
|
|
|
908
|
+
if (typeof normalizedValue === "boolean") {
|
|
909
|
+
return normalizedValue ? "1" : "0";
|
|
910
|
+
}
|
|
911
|
+
|
|
818
912
|
if (type === "BOOLEAN") {
|
|
819
|
-
if (["true", "yes", "1"].includes(normalizeIdentifierKey(
|
|
913
|
+
if (["true", "yes", "1"].includes(normalizeIdentifierKey(normalizedValue))) {
|
|
820
914
|
return "1";
|
|
821
915
|
}
|
|
822
916
|
|
|
823
|
-
if (["false", "no", "0"].includes(normalizeIdentifierKey(
|
|
917
|
+
if (["false", "no", "0"].includes(normalizeIdentifierKey(normalizedValue))) {
|
|
824
918
|
return "0";
|
|
825
919
|
}
|
|
826
920
|
}
|
|
@@ -841,8 +935,16 @@ function getImportedFillColumns(draft) {
|
|
|
841
935
|
);
|
|
842
936
|
}
|
|
843
937
|
|
|
938
|
+
function getImportedRows(draft) {
|
|
939
|
+
return Array.isArray(draft.importRows) && draft.importRows.length
|
|
940
|
+
? draft.importRows
|
|
941
|
+
: draft.importedCsvRows ?? [];
|
|
942
|
+
}
|
|
943
|
+
|
|
844
944
|
function buildImportedInsertPreviewSql(draft, maxRows = 3) {
|
|
845
|
-
|
|
945
|
+
const importRows = getImportedRows(draft);
|
|
946
|
+
|
|
947
|
+
if (!draft.fillImportedRows || !importRows.length) {
|
|
846
948
|
return [];
|
|
847
949
|
}
|
|
848
950
|
|
|
@@ -860,18 +962,18 @@ function buildImportedInsertPreviewSql(draft, maxRows = 3) {
|
|
|
860
962
|
columnSql,
|
|
861
963
|
") VALUES",
|
|
862
964
|
].join(" ");
|
|
863
|
-
const previewStatements =
|
|
965
|
+
const previewStatements = importRows.slice(0, maxRows).map((row) => {
|
|
864
966
|
const valueSql = importedColumns
|
|
865
|
-
.map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex]
|
|
967
|
+
.map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex]))
|
|
866
968
|
.join(", ");
|
|
867
969
|
|
|
868
970
|
return [insertPrefix, "(", valueSql, ");"].join(" ");
|
|
869
971
|
});
|
|
870
972
|
|
|
871
|
-
if (
|
|
973
|
+
if (importRows.length > maxRows) {
|
|
872
974
|
previewStatements.push(
|
|
873
|
-
`-- ... ${
|
|
874
|
-
|
|
975
|
+
`-- ... ${importRows.length - maxRows} more imported row${
|
|
976
|
+
importRows.length - maxRows === 1 ? "" : "s"
|
|
875
977
|
}`
|
|
876
978
|
);
|
|
877
979
|
}
|
|
@@ -960,6 +1062,10 @@ function parseCsvRows(text, delimiter = ",", maxRows = 16) {
|
|
|
960
1062
|
cell += character;
|
|
961
1063
|
}
|
|
962
1064
|
|
|
1065
|
+
if (inQuotes) {
|
|
1066
|
+
throw new Error("Delimited text contains an unterminated quoted value.");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
963
1069
|
if (cell.length || row.length) {
|
|
964
1070
|
pushRow();
|
|
965
1071
|
}
|
|
@@ -1039,6 +1145,16 @@ function suggestImportedTableName(fileName, catalogTables = []) {
|
|
|
1039
1145
|
return buildUniqueName(normalizeImportedTableName(fileName), existingNames, "imported_table");
|
|
1040
1146
|
}
|
|
1041
1147
|
|
|
1148
|
+
function inferImportFormatFromFileName(fileName) {
|
|
1149
|
+
const extension = normalizeIdentifierKey(String(fileName ?? "").split(".").pop());
|
|
1150
|
+
|
|
1151
|
+
if (["csv", "tsv", "json"].includes(extension)) {
|
|
1152
|
+
return extension;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return "csv";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1042
1158
|
function isBooleanSample(value) {
|
|
1043
1159
|
return ["true", "false", "yes", "no"].includes(normalizeIdentifierKey(value));
|
|
1044
1160
|
}
|
|
@@ -1076,8 +1192,26 @@ function isDateTimeSample(value) {
|
|
|
1076
1192
|
return !Number.isNaN(Date.parse(normalized.replace(" ", "T")));
|
|
1077
1193
|
}
|
|
1078
1194
|
|
|
1079
|
-
function
|
|
1080
|
-
|
|
1195
|
+
function isNumericImportValue(value) {
|
|
1196
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function isIntegerImportValue(value) {
|
|
1200
|
+
return isNumericImportValue(value) && Number.isInteger(value);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export function inferSQLiteType(values = [], { columnName = "" } = {}) {
|
|
1204
|
+
const nonEmptyValues = values.filter((value) => {
|
|
1205
|
+
if (value === null || value === undefined) {
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (typeof value === "string") {
|
|
1210
|
+
return Boolean(normalizeTrimmed(value));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return true;
|
|
1214
|
+
});
|
|
1081
1215
|
const normalizedColumnName = normalizeIdentifierKey(columnName);
|
|
1082
1216
|
|
|
1083
1217
|
if (normalizedColumnName === "id") {
|
|
@@ -1088,95 +1222,258 @@ function inferImportedColumnType(columnName, sampleValues) {
|
|
|
1088
1222
|
return normalizedColumnName.endsWith("_id") ? "INTEGER" : "TEXT";
|
|
1089
1223
|
}
|
|
1090
1224
|
|
|
1225
|
+
if (nonEmptyValues.some((value) => typeof value === "object")) {
|
|
1226
|
+
return "TEXT";
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (nonEmptyValues.every((value) => typeof value === "boolean")) {
|
|
1230
|
+
return "INTEGER";
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (nonEmptyValues.every(isIntegerImportValue)) {
|
|
1234
|
+
return "INTEGER";
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (nonEmptyValues.every(isNumericImportValue)) {
|
|
1238
|
+
return "REAL";
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const stringValues = nonEmptyValues.map(normalizeTrimmed);
|
|
1242
|
+
|
|
1091
1243
|
if (nonEmptyValues.every(isDateTimeSample)) {
|
|
1092
1244
|
return "DATETIME";
|
|
1093
1245
|
}
|
|
1094
1246
|
|
|
1095
|
-
if (
|
|
1247
|
+
if (stringValues.every(isDateSample)) {
|
|
1096
1248
|
return "DATE";
|
|
1097
1249
|
}
|
|
1098
1250
|
|
|
1099
|
-
if (
|
|
1251
|
+
if (stringValues.every(isBooleanSample)) {
|
|
1100
1252
|
return "BOOLEAN";
|
|
1101
1253
|
}
|
|
1102
1254
|
|
|
1103
|
-
if (
|
|
1255
|
+
if (stringValues.every(isIntegerSample)) {
|
|
1104
1256
|
return "INTEGER";
|
|
1105
1257
|
}
|
|
1106
1258
|
|
|
1107
|
-
if (
|
|
1259
|
+
if (stringValues.every((value) => isIntegerSample(value) || isRealSample(value))) {
|
|
1108
1260
|
return "REAL";
|
|
1109
1261
|
}
|
|
1110
1262
|
|
|
1111
1263
|
return "TEXT";
|
|
1112
1264
|
}
|
|
1113
1265
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
) {
|
|
1118
|
-
const delimiter = detectCsvDelimiter(csvText);
|
|
1119
|
-
const rows = parseCsvRows(csvText, delimiter, Number.POSITIVE_INFINITY).filter(
|
|
1120
|
-
(row) => !isCsvRowEmpty(row)
|
|
1121
|
-
);
|
|
1266
|
+
function isPlainImportObject(value) {
|
|
1267
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1268
|
+
}
|
|
1122
1269
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1270
|
+
function normalizeJsonImportValue(value) {
|
|
1271
|
+
if (value === null || value === undefined) {
|
|
1272
|
+
return null;
|
|
1125
1273
|
}
|
|
1126
1274
|
|
|
1127
|
-
|
|
1275
|
+
if (typeof value === "boolean") {
|
|
1276
|
+
return value ? 1 : 0;
|
|
1277
|
+
}
|
|
1128
1278
|
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1279
|
+
if (typeof value === "number") {
|
|
1280
|
+
return Number.isFinite(value) ? value : String(value);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (typeof value === "string") {
|
|
1284
|
+
return value;
|
|
1131
1285
|
}
|
|
1132
1286
|
|
|
1133
|
-
|
|
1287
|
+
return normalizeImportedCellValue(value);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function buildImportColumns(sourceNames, importRows) {
|
|
1134
1291
|
const usedColumnNames = new Set();
|
|
1135
1292
|
let primaryKeyAssigned = false;
|
|
1136
1293
|
|
|
1137
|
-
|
|
1294
|
+
return sourceNames.map((sourceName, index) => {
|
|
1138
1295
|
const fallbackName = `column_${index + 1}`;
|
|
1139
|
-
const
|
|
1140
|
-
normalizeImportedColumnName(
|
|
1296
|
+
const targetName = buildUniqueName(
|
|
1297
|
+
normalizeImportedColumnName(sourceName) || fallbackName,
|
|
1141
1298
|
usedColumnNames,
|
|
1142
1299
|
fallbackName
|
|
1143
1300
|
);
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
const isPrimaryKey = !primaryKeyAssigned &&
|
|
1301
|
+
const values = importRows.map((row) => row[index]);
|
|
1302
|
+
const sourceKey = normalizeIdentifierKey(sourceName);
|
|
1303
|
+
const isPrimaryKey = !primaryKeyAssigned && sourceKey === "id";
|
|
1147
1304
|
|
|
1148
1305
|
if (isPrimaryKey) {
|
|
1149
1306
|
primaryKeyAssigned = true;
|
|
1150
1307
|
}
|
|
1151
1308
|
|
|
1152
|
-
return
|
|
1153
|
-
|
|
1154
|
-
|
|
1309
|
+
return {
|
|
1310
|
+
sourceName,
|
|
1311
|
+
targetName,
|
|
1312
|
+
inferredType: isPrimaryKey ? "INTEGER" : inferSQLiteType(values, { columnName: targetName }),
|
|
1313
|
+
nullable: values.some((value) => value === null || value === undefined || normalizeTrimmed(value) === ""),
|
|
1155
1314
|
primaryKey: isPrimaryKey,
|
|
1156
|
-
|
|
1157
|
-
|
|
1315
|
+
};
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function createDelimitedImportDataset({ fileName = "", text = "", format = "csv" } = {}) {
|
|
1320
|
+
const importFormat = normalizeImportFormat(format);
|
|
1321
|
+
const delimiter = importFormat === "tsv" ? "\t" : detectCsvDelimiter(text);
|
|
1322
|
+
const rows = parseCsvRows(text, delimiter, Number.POSITIVE_INFINITY).filter(
|
|
1323
|
+
(row) => !isCsvRowEmpty(row)
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
if (!rows.length) {
|
|
1327
|
+
throw new Error(`The selected ${importFormat.toUpperCase()} file is empty.`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const headerRow = rows[0].map((cell) => normalizeImportedColumnName(cell));
|
|
1331
|
+
|
|
1332
|
+
if (!headerRow.some((cell) => normalizeTrimmed(cell))) {
|
|
1333
|
+
throw new Error(`The ${importFormat.toUpperCase()} header row is empty.`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const dataRows = rows.slice(1).filter((row) => !isCsvRowEmpty(row));
|
|
1337
|
+
const headerWidth = headerRow.length;
|
|
1338
|
+
const overflowRow = dataRows.find((row) => row.length > headerWidth);
|
|
1339
|
+
|
|
1340
|
+
if (overflowRow) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`${importFormat.toUpperCase()} row has more values than the header row.`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const importRows = dataRows.map((row) => headerRow.map((_, index) => row[index] ?? ""));
|
|
1347
|
+
const sourceNames = headerRow.map((headerCell, index) => headerCell || `column_${index + 1}`);
|
|
1348
|
+
const columns = buildImportColumns(sourceNames, importRows);
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
format: importFormat,
|
|
1352
|
+
sourceFileName: normalizeText(fileName),
|
|
1353
|
+
suggestedTableName: normalizeImportedTableName(fileName),
|
|
1354
|
+
delimiter,
|
|
1355
|
+
columns,
|
|
1356
|
+
previewRows: importRows.slice(0, 16),
|
|
1357
|
+
rows: importRows,
|
|
1358
|
+
rowCount: importRows.length,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function createJsonImportDataset({ fileName = "", text = "" } = {}) {
|
|
1363
|
+
const sourceText = normalizeText(text).replace(/^\uFEFF/, "").trim();
|
|
1364
|
+
|
|
1365
|
+
if (!sourceText) {
|
|
1366
|
+
throw new Error("The selected JSON file is empty.");
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
let parsed;
|
|
1370
|
+
|
|
1371
|
+
try {
|
|
1372
|
+
parsed = JSON.parse(sourceText);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
throw new Error(`INVALID_JSON: ${error.message}`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
let records;
|
|
1378
|
+
|
|
1379
|
+
if (Array.isArray(parsed)) {
|
|
1380
|
+
if (!parsed.length) {
|
|
1381
|
+
throw new Error("EMPTY_JSON_ARRAY_IS_NOT_SUPPORTED");
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!parsed.every(isPlainImportObject)) {
|
|
1385
|
+
throw new Error("MIXED_ARRAY_VALUES_ARE_NOT_SUPPORTED");
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
records = parsed;
|
|
1389
|
+
} else if (isPlainImportObject(parsed)) {
|
|
1390
|
+
records = [parsed];
|
|
1391
|
+
} else {
|
|
1392
|
+
throw new Error("JSON_ROOT_VALUE_IS_NOT_SUPPORTED");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const sourceNames = [];
|
|
1396
|
+
const seenKeys = new Set();
|
|
1397
|
+
|
|
1398
|
+
records.forEach((record) => {
|
|
1399
|
+
Object.keys(record).forEach((key) => {
|
|
1400
|
+
if (seenKeys.has(key)) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
seenKeys.add(key);
|
|
1405
|
+
sourceNames.push(key);
|
|
1158
1406
|
});
|
|
1159
1407
|
});
|
|
1160
1408
|
|
|
1161
|
-
|
|
1162
|
-
.
|
|
1163
|
-
|
|
1164
|
-
|
|
1409
|
+
if (!sourceNames.length) {
|
|
1410
|
+
throw new Error("JSON records do not contain importable properties.");
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const importRows = records.map((record) =>
|
|
1414
|
+
sourceNames.map((sourceName) =>
|
|
1415
|
+
Object.prototype.hasOwnProperty.call(record, sourceName)
|
|
1416
|
+
? normalizeJsonImportValue(record[sourceName])
|
|
1417
|
+
: null
|
|
1418
|
+
)
|
|
1419
|
+
);
|
|
1420
|
+
const columns = buildImportColumns(sourceNames, importRows);
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
format: "json",
|
|
1424
|
+
sourceFileName: normalizeText(fileName),
|
|
1425
|
+
suggestedTableName: normalizeImportedTableName(fileName),
|
|
1426
|
+
delimiter: "",
|
|
1427
|
+
columns,
|
|
1428
|
+
previewRows: importRows.slice(0, 16),
|
|
1429
|
+
rows: importRows,
|
|
1430
|
+
rowCount: importRows.length,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
export function createImportDatasetFromText({ fileName = "", text = "", format = "" } = {}) {
|
|
1435
|
+
const importFormat = normalizeImportFormat(format || inferImportFormatFromFileName(fileName));
|
|
1436
|
+
|
|
1437
|
+
if (importFormat === "json") {
|
|
1438
|
+
return createJsonImportDataset({ fileName, text });
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return createDelimitedImportDataset({ fileName, text, format: importFormat });
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
export function createTableDesignerDraftFromImport(
|
|
1445
|
+
{ fileName = "", text = "", format = "" },
|
|
1446
|
+
{ catalogTables = [], supportedTypes = DEFAULT_TABLE_DESIGNER_TYPES, readOnly = false } = {}
|
|
1447
|
+
) {
|
|
1448
|
+
const dataset = createImportDatasetFromText({ fileName, text, format });
|
|
1449
|
+
const columns = dataset.columns.map((column, index) =>
|
|
1450
|
+
createEmptyTableDesignerColumn({
|
|
1451
|
+
name: column.targetName,
|
|
1452
|
+
type: column.inferredType,
|
|
1453
|
+
primaryKey: column.primaryKey,
|
|
1454
|
+
notNull: column.primaryKey,
|
|
1455
|
+
importedValueIndex: index,
|
|
1456
|
+
})
|
|
1457
|
+
);
|
|
1165
1458
|
|
|
1166
1459
|
const draft = recalculateTableDesignerDraft(
|
|
1167
1460
|
{
|
|
1168
1461
|
mode: "create",
|
|
1169
1462
|
originalTableName: "",
|
|
1170
|
-
tableName: suggestImportedTableName(
|
|
1463
|
+
tableName: suggestImportedTableName(dataset.sourceFileName, catalogTables),
|
|
1171
1464
|
columns,
|
|
1172
1465
|
uniqueConstraints: [],
|
|
1173
1466
|
checkConstraints: [],
|
|
1174
1467
|
designerVersion: 2,
|
|
1175
1468
|
schemaWarnings: [],
|
|
1176
|
-
fillImportedRows:
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1469
|
+
fillImportedRows: dataset.rows.length > 0,
|
|
1470
|
+
importFormat: dataset.format,
|
|
1471
|
+
importSourceFileName: dataset.sourceFileName,
|
|
1472
|
+
importDelimiter: dataset.delimiter,
|
|
1473
|
+
importRows: dataset.rows,
|
|
1474
|
+
importedCsvFileName: dataset.sourceFileName,
|
|
1475
|
+
importedCsvDelimiter: dataset.delimiter,
|
|
1476
|
+
importedCsvRows: dataset.rows,
|
|
1180
1477
|
},
|
|
1181
1478
|
{ catalogTables, supportedTypes, readOnly }
|
|
1182
1479
|
);
|
|
@@ -1184,12 +1481,24 @@ export function createTableDesignerDraftFromCsvImport(
|
|
|
1184
1481
|
return {
|
|
1185
1482
|
draft,
|
|
1186
1483
|
columnCount: columns.length,
|
|
1187
|
-
delimiter,
|
|
1188
|
-
|
|
1189
|
-
|
|
1484
|
+
delimiter: dataset.delimiter,
|
|
1485
|
+
format: dataset.format,
|
|
1486
|
+
importedRowCount: dataset.rowCount,
|
|
1487
|
+
dataset,
|
|
1488
|
+
sampleRowCount: Math.min(dataset.rowCount, 16),
|
|
1190
1489
|
};
|
|
1191
1490
|
}
|
|
1192
1491
|
|
|
1492
|
+
export function createTableDesignerDraftFromCsvImport(
|
|
1493
|
+
{ fileName = "", csvText = "" },
|
|
1494
|
+
context = {}
|
|
1495
|
+
) {
|
|
1496
|
+
return createTableDesignerDraftFromImport(
|
|
1497
|
+
{ fileName, text: csvText, format: "csv" },
|
|
1498
|
+
context
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1193
1502
|
export function createNewTableDesignerDraft() {
|
|
1194
1503
|
return {
|
|
1195
1504
|
mode: "create",
|
|
@@ -1202,6 +1511,10 @@ export function createNewTableDesignerDraft() {
|
|
|
1202
1511
|
dirty: false,
|
|
1203
1512
|
schemaWarnings: [],
|
|
1204
1513
|
fillImportedRows: false,
|
|
1514
|
+
importFormat: "",
|
|
1515
|
+
importSourceFileName: "",
|
|
1516
|
+
importDelimiter: "",
|
|
1517
|
+
importRows: [],
|
|
1205
1518
|
importedCsvFileName: "",
|
|
1206
1519
|
importedCsvDelimiter: "",
|
|
1207
1520
|
importedCsvRows: [],
|
|
@@ -1323,6 +1636,62 @@ export function updateTableDesignerConstraintField(
|
|
|
1323
1636
|
);
|
|
1324
1637
|
}
|
|
1325
1638
|
|
|
1639
|
+
export function addTableDesignerCheckConstraint(draft, seed = {}, context = {}) {
|
|
1640
|
+
const nextIndex = (draft.checkConstraints ?? []).length + 1;
|
|
1641
|
+
const checkConstraint = {
|
|
1642
|
+
id: normalizeText(seed.id ?? `check:new:${createColumnId()}`),
|
|
1643
|
+
name: normalizeText(seed.name ?? `CHECK ${nextIndex}`),
|
|
1644
|
+
originalName: "",
|
|
1645
|
+
deleted: false,
|
|
1646
|
+
columns: Array.isArray(seed.columns) ? seed.columns : [],
|
|
1647
|
+
expression: normalizeText(seed.expression ?? ""),
|
|
1648
|
+
originalExpression: "",
|
|
1649
|
+
editable: true,
|
|
1650
|
+
preserved: false,
|
|
1651
|
+
columnId: normalizeText(seed.columnId ?? ""),
|
|
1652
|
+
source: normalizeText(seed.source ?? "user"),
|
|
1653
|
+
presetId: normalizeText(seed.presetId ?? ""),
|
|
1654
|
+
presetFields:
|
|
1655
|
+
seed.presetFields && typeof seed.presetFields === "object"
|
|
1656
|
+
? Object.fromEntries(
|
|
1657
|
+
Object.entries(seed.presetFields).map(([key, value]) => [key, normalizeText(value)])
|
|
1658
|
+
)
|
|
1659
|
+
: {},
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
return recalculateTableDesignerDraft(
|
|
1663
|
+
{
|
|
1664
|
+
...draft,
|
|
1665
|
+
checkConstraints: [...(draft.checkConstraints ?? []), checkConstraint],
|
|
1666
|
+
},
|
|
1667
|
+
context
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
export function removeTableDesignerCheckConstraint(draft, constraintId, context = {}) {
|
|
1672
|
+
const targetConstraint = (draft.checkConstraints ?? []).find(
|
|
1673
|
+
(constraint) => constraint.id === constraintId
|
|
1674
|
+
);
|
|
1675
|
+
|
|
1676
|
+
if (!targetConstraint) {
|
|
1677
|
+
return recalculateTableDesignerDraft(draft, context);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const nextCheckConstraints = targetConstraint.originalExpression
|
|
1681
|
+
? (draft.checkConstraints ?? []).map((constraint) =>
|
|
1682
|
+
constraint.id === constraintId ? { ...constraint, deleted: true } : constraint
|
|
1683
|
+
)
|
|
1684
|
+
: (draft.checkConstraints ?? []).filter((constraint) => constraint.id !== constraintId);
|
|
1685
|
+
|
|
1686
|
+
return recalculateTableDesignerDraft(
|
|
1687
|
+
{
|
|
1688
|
+
...draft,
|
|
1689
|
+
checkConstraints: nextCheckConstraints,
|
|
1690
|
+
},
|
|
1691
|
+
context
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1326
1695
|
export function addTableDesignerColumn(draft, context = {}) {
|
|
1327
1696
|
return recalculateTableDesignerDraft(
|
|
1328
1697
|
{
|