sqlite-hub 0.11.1 → 0.16.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 +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
|
@@ -74,6 +74,95 @@ function buildSimpleForeignKeyMap(foreignKeys = []) {
|
|
|
74
74
|
return map;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function buildUniqueConstraintExpression(index = {}) {
|
|
78
|
+
const columns = (index.columns ?? [])
|
|
79
|
+
.map((column) => column?.name)
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
|
|
82
|
+
if (!columns.length) {
|
|
83
|
+
return "UNIQUE constraint";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `UNIQUE (${columns.map((columnName) => quoteIdentifier(columnName)).join(", ")})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mapComplexUniqueConstraint(index = {}) {
|
|
90
|
+
const sql = String(index.sql ?? "").trim();
|
|
91
|
+
const columns = (index.columns ?? [])
|
|
92
|
+
.map((column) => ({
|
|
93
|
+
name: column.name ?? "",
|
|
94
|
+
descending: Boolean(column.descending),
|
|
95
|
+
collation: column.collation ?? "",
|
|
96
|
+
}))
|
|
97
|
+
.filter((column) => column.name);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: `unique:${index.name}`,
|
|
101
|
+
name: index.name ?? "",
|
|
102
|
+
originalName: index.name ?? "",
|
|
103
|
+
columns,
|
|
104
|
+
partial: Boolean(index.partial),
|
|
105
|
+
origin: index.origin ?? "",
|
|
106
|
+
sql,
|
|
107
|
+
originalSql: sql,
|
|
108
|
+
expression: sql || buildUniqueConstraintExpression(index),
|
|
109
|
+
originalExpression: sql || buildUniqueConstraintExpression(index),
|
|
110
|
+
editable: true,
|
|
111
|
+
preserved: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildComplexUniqueConstraints(indexes = []) {
|
|
116
|
+
return indexes
|
|
117
|
+
.filter((index) => index?.unique && (index.partial || (index.columns?.length ?? 0) !== 1))
|
|
118
|
+
.map(mapComplexUniqueConstraint);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function expressionMentionsColumn(expression = "", columnName = "") {
|
|
122
|
+
const normalizedExpression = String(expression ?? "").toLowerCase();
|
|
123
|
+
const normalizedColumn = String(columnName ?? "").toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (!normalizedColumn) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
new RegExp(`(^|[^a-z0-9_$])${normalizedColumn.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9_$]|$)`).test(
|
|
131
|
+
normalizedExpression
|
|
132
|
+
) ||
|
|
133
|
+
normalizedExpression.includes(`"${normalizedColumn.replaceAll('"', '""')}"`) ||
|
|
134
|
+
normalizedExpression.includes(`\`${normalizedColumn.replaceAll("`", "``")}\``) ||
|
|
135
|
+
normalizedExpression.includes(`[${normalizedColumn}]`)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildCheckConstraints(tableDetail) {
|
|
140
|
+
const visibleColumns = (tableDetail.columns ?? []).filter(
|
|
141
|
+
(column) => column.visible !== false && !column.generated
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return (tableDetail.checkConstraints ?? []).map((constraint, index) => {
|
|
145
|
+
const expression = String(constraint.expression ?? "").trim();
|
|
146
|
+
const columns = visibleColumns
|
|
147
|
+
.filter((column) => expressionMentionsColumn(expression, column.name))
|
|
148
|
+
.map((column) => ({
|
|
149
|
+
name: column.name,
|
|
150
|
+
allowedValues: Array.isArray(column.allowedValues) ? column.allowedValues : [],
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
id: `check:${constraint.id ?? index}`,
|
|
155
|
+
name: `CHECK ${index + 1}`,
|
|
156
|
+
originalName: `CHECK ${index + 1}`,
|
|
157
|
+
columns,
|
|
158
|
+
expression: expression ? `CHECK (${expression})` : "CHECK constraint",
|
|
159
|
+
originalExpression: expression ? `CHECK (${expression})` : "CHECK constraint",
|
|
160
|
+
editable: true,
|
|
161
|
+
preserved: true,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
77
166
|
function buildSchemaWarnings(tableDetail) {
|
|
78
167
|
const warnings = [];
|
|
79
168
|
const generatedColumns = (tableDetail.columns ?? []).filter((column) => column.generated);
|
|
@@ -83,9 +172,7 @@ function buildSchemaWarnings(tableDetail) {
|
|
|
83
172
|
const complexForeignKeys = (tableDetail.foreignKeys ?? []).filter(
|
|
84
173
|
(foreignKey) => (foreignKey.mappings?.length ?? 0) !== 1
|
|
85
174
|
);
|
|
86
|
-
const
|
|
87
|
-
(index) => index.unique && (index.partial || (index.columns?.length ?? 0) !== 1)
|
|
88
|
-
);
|
|
175
|
+
const complexUniqueConstraints = buildComplexUniqueConstraints(tableDetail.indexes);
|
|
89
176
|
|
|
90
177
|
if (generatedColumns.length) {
|
|
91
178
|
warnings.push(
|
|
@@ -93,7 +180,7 @@ function buildSchemaWarnings(tableDetail) {
|
|
|
93
180
|
code: "GENERATED_COLUMNS_PRESENT",
|
|
94
181
|
title: "Generated Columns Detected",
|
|
95
182
|
message:
|
|
96
|
-
"Generated or hidden columns are not editable in Table Designer
|
|
183
|
+
"Generated or hidden columns are inspectable but not editable in Table Designer v2. Safe operations like table rename or adding simple columns still work.",
|
|
97
184
|
})
|
|
98
185
|
);
|
|
99
186
|
}
|
|
@@ -104,7 +191,7 @@ function buildSchemaWarnings(tableDetail) {
|
|
|
104
191
|
code: "COMPOSITE_PRIMARY_KEY_PRESENT",
|
|
105
192
|
title: "Composite Primary Key Detected",
|
|
106
193
|
message:
|
|
107
|
-
"This table uses more than one primary key column. Table Designer
|
|
194
|
+
"This table uses more than one primary key column. Table Designer v2 preserves it, but changing primary key structure requires a manual table rebuild.",
|
|
108
195
|
})
|
|
109
196
|
);
|
|
110
197
|
}
|
|
@@ -115,18 +202,19 @@ function buildSchemaWarnings(tableDetail) {
|
|
|
115
202
|
code: "COMPLEX_FOREIGN_KEYS_PRESENT",
|
|
116
203
|
title: "Complex Foreign Keys Detected",
|
|
117
204
|
message:
|
|
118
|
-
"Composite or multi-mapping foreign keys cannot be edited directly in Table Designer
|
|
205
|
+
"Composite or multi-mapping foreign keys cannot be edited directly in Table Designer v2. They are preserved until a rebuild is done manually.",
|
|
119
206
|
})
|
|
120
207
|
);
|
|
121
208
|
}
|
|
122
209
|
|
|
123
|
-
if (
|
|
210
|
+
if (complexUniqueConstraints.length) {
|
|
124
211
|
warnings.push(
|
|
125
212
|
createDesignerWarning({
|
|
126
213
|
code: "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT",
|
|
127
|
-
title: "
|
|
214
|
+
title: "Table Designer v2 Unique Constraints",
|
|
128
215
|
message:
|
|
129
|
-
"Multi-column
|
|
216
|
+
"Multi-column and partial UNIQUE constraints are detected, shown in the v2 constraints panel, and preserved by SQLite-safe saves. Editing them still requires manual SQL review.",
|
|
217
|
+
tone: "muted",
|
|
130
218
|
})
|
|
131
219
|
);
|
|
132
220
|
}
|
|
@@ -149,7 +237,7 @@ function buildSchemaWarnings(tableDetail) {
|
|
|
149
237
|
code: "WITHOUT_ROWID_PRESENT",
|
|
150
238
|
title: "WITHOUT ROWID Table",
|
|
151
239
|
message:
|
|
152
|
-
"WITHOUT ROWID tables can be inspected here, but rebuild-style changes are intentionally blocked in
|
|
240
|
+
"WITHOUT ROWID tables can be inspected here, but rebuild-style changes are intentionally blocked in v2.",
|
|
153
241
|
tone: "muted",
|
|
154
242
|
})
|
|
155
243
|
);
|
|
@@ -189,6 +277,8 @@ function mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }) {
|
|
|
189
277
|
function buildTableDesignerDraft(tableDetail) {
|
|
190
278
|
const uniqueColumns = buildSingleColumnUniqueSet(tableDetail.indexes);
|
|
191
279
|
const foreignKeyMap = buildSimpleForeignKeyMap(tableDetail.foreignKeys);
|
|
280
|
+
const uniqueConstraints = buildComplexUniqueConstraints(tableDetail.indexes);
|
|
281
|
+
const checkConstraints = buildCheckConstraints(tableDetail);
|
|
192
282
|
const columns = (tableDetail.columns ?? [])
|
|
193
283
|
.filter((column) => column.visible !== false && !column.generated)
|
|
194
284
|
.map((column) => mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }));
|
|
@@ -199,6 +289,9 @@ function buildTableDesignerDraft(tableDetail) {
|
|
|
199
289
|
originalTableName: tableDetail.name,
|
|
200
290
|
tableName: tableDetail.name,
|
|
201
291
|
columns,
|
|
292
|
+
uniqueConstraints,
|
|
293
|
+
checkConstraints,
|
|
294
|
+
designerVersion: 2,
|
|
202
295
|
dirty: false,
|
|
203
296
|
schemaWarnings,
|
|
204
297
|
warnings: [...schemaWarnings],
|
|
@@ -227,6 +320,8 @@ function listDesignerTables(db) {
|
|
|
227
320
|
module.exports = {
|
|
228
321
|
SUPPORTED_TABLE_DESIGNER_TYPES,
|
|
229
322
|
buildTableDesignerDraft,
|
|
323
|
+
buildComplexUniqueConstraints,
|
|
324
|
+
buildCheckConstraints,
|
|
230
325
|
createDesignerWarning,
|
|
231
326
|
listDesignerTables,
|
|
232
327
|
normalizeDesignerType,
|
|
@@ -66,6 +66,53 @@ function normalizeColumnPayload(column = {}, index = 0) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function normalizeUniqueConstraintPayload(constraint = {}, index = 0) {
|
|
70
|
+
return {
|
|
71
|
+
id: String(constraint.id ?? `unique:${index}`),
|
|
72
|
+
name: String(constraint.name ?? "").trim(),
|
|
73
|
+
originalName: String(constraint.originalName ?? constraint.name ?? "").trim(),
|
|
74
|
+
columns: Array.isArray(constraint.columns)
|
|
75
|
+
? constraint.columns
|
|
76
|
+
.map((column) => ({
|
|
77
|
+
name: String(column?.name ?? "").trim(),
|
|
78
|
+
descending: normalizeBoolean(column?.descending),
|
|
79
|
+
collation: String(column?.collation ?? "").trim(),
|
|
80
|
+
}))
|
|
81
|
+
.filter((column) => column.name)
|
|
82
|
+
: [],
|
|
83
|
+
partial: normalizeBoolean(constraint.partial),
|
|
84
|
+
origin: String(constraint.origin ?? "").trim(),
|
|
85
|
+
sql: String(constraint.sql ?? "").trim(),
|
|
86
|
+
originalSql: String(constraint.originalSql ?? constraint.sql ?? "").trim(),
|
|
87
|
+
expression: String(constraint.expression ?? "").trim(),
|
|
88
|
+
originalExpression: String(constraint.originalExpression ?? constraint.expression ?? constraint.sql ?? "").trim(),
|
|
89
|
+
editable: normalizeBoolean(constraint.editable),
|
|
90
|
+
preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeCheckConstraintPayload(constraint = {}, index = 0) {
|
|
95
|
+
return {
|
|
96
|
+
id: String(constraint.id ?? `check:${index}`),
|
|
97
|
+
name: String(constraint.name ?? `CHECK ${index + 1}`).trim(),
|
|
98
|
+
originalName: String(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`).trim(),
|
|
99
|
+
columns: Array.isArray(constraint.columns)
|
|
100
|
+
? constraint.columns
|
|
101
|
+
.map((column) => ({
|
|
102
|
+
name: String(column?.name ?? "").trim(),
|
|
103
|
+
allowedValues: Array.isArray(column?.allowedValues)
|
|
104
|
+
? column.allowedValues.map((value) => String(value ?? ""))
|
|
105
|
+
: [],
|
|
106
|
+
}))
|
|
107
|
+
.filter((column) => column.name)
|
|
108
|
+
: [],
|
|
109
|
+
expression: String(constraint.expression ?? "").trim(),
|
|
110
|
+
originalExpression: String(constraint.originalExpression ?? constraint.expression ?? "").trim(),
|
|
111
|
+
editable: normalizeBoolean(constraint.editable),
|
|
112
|
+
preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
function normalizeDraftPayload(payload = {}) {
|
|
70
117
|
const mode = String(payload.mode ?? "create").trim() === "edit" ? "edit" : "create";
|
|
71
118
|
|
|
@@ -76,6 +123,17 @@ function normalizeDraftPayload(payload = {}) {
|
|
|
76
123
|
columns: Array.isArray(payload.columns)
|
|
77
124
|
? payload.columns.map((column, index) => normalizeColumnPayload(column, index))
|
|
78
125
|
: [],
|
|
126
|
+
uniqueConstraints: Array.isArray(payload.uniqueConstraints)
|
|
127
|
+
? payload.uniqueConstraints.map((constraint, index) =>
|
|
128
|
+
normalizeUniqueConstraintPayload(constraint, index)
|
|
129
|
+
)
|
|
130
|
+
: [],
|
|
131
|
+
checkConstraints: Array.isArray(payload.checkConstraints)
|
|
132
|
+
? payload.checkConstraints.map((constraint, index) =>
|
|
133
|
+
normalizeCheckConstraintPayload(constraint, index)
|
|
134
|
+
)
|
|
135
|
+
: [],
|
|
136
|
+
designerVersion: Number(payload.designerVersion) || 1,
|
|
79
137
|
schemaWarnings: Array.isArray(payload.schemaWarnings) ? payload.schemaWarnings : [],
|
|
80
138
|
warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
|
|
81
139
|
fillImportedRows: normalizeBoolean(payload.fillImportedRows),
|
|
@@ -112,7 +170,7 @@ function validatePrimaryKeys(draft, originalDraft) {
|
|
|
112
170
|
|
|
113
171
|
if (!originalDraft) {
|
|
114
172
|
throw new ValidationError(
|
|
115
|
-
"Table Designer
|
|
173
|
+
"Table Designer v2 supports a single primary key column when creating a table."
|
|
116
174
|
);
|
|
117
175
|
}
|
|
118
176
|
|
|
@@ -134,7 +192,7 @@ function validatePrimaryKeys(draft, originalDraft) {
|
|
|
134
192
|
|
|
135
193
|
if (!isUnchangedCompositePrimaryKey) {
|
|
136
194
|
throw new ValidationError(
|
|
137
|
-
"Table Designer
|
|
195
|
+
"Table Designer v2 does not support editing composite primary keys."
|
|
138
196
|
);
|
|
139
197
|
}
|
|
140
198
|
}
|
|
@@ -109,7 +109,7 @@ class TableDesignerService {
|
|
|
109
109
|
|
|
110
110
|
if (!analysis.executable) {
|
|
111
111
|
throw new ValidationError(
|
|
112
|
-
"This schema change set would require a SQLite table rebuild. Table Designer
|
|
112
|
+
"This schema change set would require a SQLite table rebuild. Table Designer v2 keeps the SQL preview available but will not execute those changes automatically.",
|
|
113
113
|
{
|
|
114
114
|
code: "TABLE_DESIGNER_REBUILD_REQUIRED",
|
|
115
115
|
warnings: analysis.warnings,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { ValidationError } = require("../../utils/errors");
|
|
2
|
+
const { quoteIdentifier } = require("../../utils/identifier");
|
|
3
|
+
|
|
4
|
+
const FILTER_OPERATORS = new Set(["=", "!=", "<", ">", "<=", ">=", "equals"]);
|
|
5
|
+
|
|
6
|
+
function escapeLikePattern(value) {
|
|
7
|
+
return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isTextColumn(column) {
|
|
11
|
+
return String(column?.affinity ?? "").toUpperCase() === "TEXT";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeTableFilter(tableDetail, options = {}) {
|
|
15
|
+
const columnName = String(options.filterColumn ?? "").trim();
|
|
16
|
+
const operator = String(options.filterOperator ?? "=").trim();
|
|
17
|
+
const value = options.filterValue;
|
|
18
|
+
|
|
19
|
+
if (!columnName || value === undefined || value === null || String(value).trim() === "") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!FILTER_OPERATORS.has(operator)) {
|
|
24
|
+
throw new ValidationError(
|
|
25
|
+
`filterOperator must be one of: ${Array.from(FILTER_OPERATORS).join(", ")}.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filterColumn = tableDetail.columns.find(
|
|
30
|
+
(column) => column.visible && column.name === columnName
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!filterColumn) {
|
|
34
|
+
throw new ValidationError(`Unknown filter column: ${columnName}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedValue = String(value);
|
|
38
|
+
const quotedColumn = quoteIdentifier(filterColumn.name);
|
|
39
|
+
|
|
40
|
+
if (operator === "equals") {
|
|
41
|
+
return {
|
|
42
|
+
column: filterColumn.name,
|
|
43
|
+
operator,
|
|
44
|
+
value: normalizedValue,
|
|
45
|
+
matchMode: "equals",
|
|
46
|
+
clause: `${quotedColumn}${isTextColumn(filterColumn) ? " COLLATE NOCASE" : ""} = ?`,
|
|
47
|
+
params: [normalizedValue],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isTextColumn(filterColumn) && (operator === "=" || operator === "!=")) {
|
|
52
|
+
return {
|
|
53
|
+
column: filterColumn.name,
|
|
54
|
+
operator,
|
|
55
|
+
value: normalizedValue,
|
|
56
|
+
matchMode: operator === "=" ? "contains" : "notContains",
|
|
57
|
+
clause: `${quotedColumn} COLLATE NOCASE ${operator === "=" ? "LIKE" : "NOT LIKE"} ? ESCAPE '\\'`,
|
|
58
|
+
params: [`%${escapeLikePattern(normalizedValue)}%`],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
column: filterColumn.name,
|
|
64
|
+
operator,
|
|
65
|
+
value: normalizedValue,
|
|
66
|
+
matchMode: "comparison",
|
|
67
|
+
clause: `${quotedColumn} ${operator} ?`,
|
|
68
|
+
params: [normalizedValue],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
FILTER_OPERATORS,
|
|
74
|
+
normalizeTableFilter,
|
|
75
|
+
};
|
package/server/utils/csv.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
function
|
|
1
|
+
function stringifyDelimitedCell(value) {
|
|
2
2
|
if (value === null || value === undefined) {
|
|
3
3
|
return "";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeCsvCell(value, delimiter) {
|
|
10
|
+
const stringValue = stringifyDelimitedCell(value);
|
|
8
11
|
|
|
9
12
|
if (
|
|
10
13
|
stringValue.includes('"') ||
|
|
@@ -18,7 +21,7 @@ function escapeCsvCell(value, delimiter) {
|
|
|
18
21
|
return stringValue;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function
|
|
24
|
+
function rowsToDelimitedText({ columns, rows, delimiter = "," }) {
|
|
22
25
|
const header = columns.map((column) => escapeCsvCell(column, delimiter)).join(delimiter);
|
|
23
26
|
const body = rows.map((row) =>
|
|
24
27
|
columns
|
|
@@ -29,6 +32,29 @@ function rowsToCsv({ columns, rows, delimiter = "," }) {
|
|
|
29
32
|
return [header, ...body].join("\n");
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
function rowsToCsv({ columns, rows, delimiter = "," }) {
|
|
36
|
+
return rowsToDelimitedText({ columns, rows, delimiter });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeMarkdownCell(value) {
|
|
40
|
+
return stringifyDelimitedCell(value)
|
|
41
|
+
.replaceAll("\\", "\\\\")
|
|
42
|
+
.replaceAll("|", "\\|")
|
|
43
|
+
.replace(/\r\n|\r|\n/g, "<br>");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rowsToMarkdownTable({ columns, rows }) {
|
|
47
|
+
const header = `| ${columns.map(escapeMarkdownCell).join(" | ")} |`;
|
|
48
|
+
const separator = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
49
|
+
const body = rows.map(
|
|
50
|
+
(row) => `| ${columns.map((column) => escapeMarkdownCell(row[column])).join(" | ")} |`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return [header, separator, ...body].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
module.exports = {
|
|
33
57
|
rowsToCsv,
|
|
58
|
+
rowsToDelimitedText,
|
|
59
|
+
rowsToMarkdownTable,
|
|
34
60
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const Database = require("better-sqlite3");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const { getTableDetail } = require("../server/services/sqlite/introspection");
|
|
5
|
+
const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
|
|
6
|
+
const { buildTableDesignerDraft } = require("../server/services/sqlite/tableDesigner/schemaMapping");
|
|
7
|
+
|
|
8
|
+
test("table detail exposes string options from simple CHECK IN constraints", () => {
|
|
9
|
+
const db = new Database(":memory:");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE stream_company_mentions (
|
|
14
|
+
id INTEGER PRIMARY KEY,
|
|
15
|
+
mention_type TEXT,
|
|
16
|
+
CHECK (
|
|
17
|
+
mention_type IS NULL
|
|
18
|
+
OR mention_type IN (
|
|
19
|
+
'company',
|
|
20
|
+
'stock_company',
|
|
21
|
+
'brand',
|
|
22
|
+
'product',
|
|
23
|
+
'person',
|
|
24
|
+
'organization',
|
|
25
|
+
'terrorists',
|
|
26
|
+
'events',
|
|
27
|
+
'collective_terms',
|
|
28
|
+
'countries',
|
|
29
|
+
'none',
|
|
30
|
+
'unknown'
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
INSERT INTO stream_company_mentions (mention_type) VALUES ('company');
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const tableDetail = getTableDetail(db, "stream_company_mentions");
|
|
38
|
+
const designerDraft = buildTableDesignerDraft(tableDetail);
|
|
39
|
+
const mentionTypeColumn = tableDetail.columns.find(
|
|
40
|
+
(column) => column.name === "mention_type"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(mentionTypeColumn.allowedValues, [
|
|
44
|
+
"company",
|
|
45
|
+
"stock_company",
|
|
46
|
+
"brand",
|
|
47
|
+
"product",
|
|
48
|
+
"person",
|
|
49
|
+
"organization",
|
|
50
|
+
"terrorists",
|
|
51
|
+
"events",
|
|
52
|
+
"collective_terms",
|
|
53
|
+
"countries",
|
|
54
|
+
"none",
|
|
55
|
+
"unknown",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const executor = new SqlExecutor({
|
|
59
|
+
connectionManager: {
|
|
60
|
+
getActiveDatabase: () => db,
|
|
61
|
+
getActiveConnection: () => ({ id: "test" }),
|
|
62
|
+
},
|
|
63
|
+
appStateStore: {
|
|
64
|
+
recordQueryExecution: () => 1,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const result = executor.execute(
|
|
68
|
+
"SELECT id, mention_type FROM stream_company_mentions"
|
|
69
|
+
);
|
|
70
|
+
const editableColumn = result.editing.columns.find(
|
|
71
|
+
(column) => column.sourceColumn === "mention_type"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(editableColumn.allowedValues, mentionTypeColumn.allowedValues);
|
|
75
|
+
|
|
76
|
+
assert.equal(designerDraft.designerVersion, 2);
|
|
77
|
+
assert.equal(designerDraft.checkConstraints.length, 1);
|
|
78
|
+
assert.match(designerDraft.checkConstraints[0].expression, /CHECK/i);
|
|
79
|
+
assert.equal(designerDraft.checkConstraints[0].originalExpression, designerDraft.checkConstraints[0].expression);
|
|
80
|
+
assert.equal(designerDraft.checkConstraints[0].editable, true);
|
|
81
|
+
assert.deepEqual(designerDraft.checkConstraints[0].columns, [
|
|
82
|
+
{
|
|
83
|
+
name: "mention_type",
|
|
84
|
+
allowedValues: mentionTypeColumn.allowedValues,
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
} finally {
|
|
88
|
+
db.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const { readFileSync } = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
let exportFilenamesModulePromise = null;
|
|
7
|
+
|
|
8
|
+
function loadExportFilenamesModule() {
|
|
9
|
+
if (!exportFilenamesModulePromise) {
|
|
10
|
+
const source = readFileSync(
|
|
11
|
+
path.resolve(__dirname, "../frontend/js/utils/exportFilenames.js"),
|
|
12
|
+
"utf8"
|
|
13
|
+
);
|
|
14
|
+
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
+
|
|
16
|
+
exportFilenamesModulePromise = import(url);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return exportFilenamesModulePromise;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("text export filenames use the selected format extension", async () => {
|
|
23
|
+
const { buildTextExportFilename } = await loadExportFilenamesModule();
|
|
24
|
+
|
|
25
|
+
assert.equal(buildTextExportFilename("query-results.csv", { format: "tsv" }), "query-results.tsv");
|
|
26
|
+
assert.equal(buildTextExportFilename("white_house_live_streams", { format: "md" }), "white_house_live_streams.md");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("text export filenames sanitize unsafe names and fallback when empty", async () => {
|
|
30
|
+
const { buildTextExportFilename } = await loadExportFilenamesModule();
|
|
31
|
+
|
|
32
|
+
assert.equal(buildTextExportFilename("", { format: "csv", fallback: "table" }), "table.csv");
|
|
33
|
+
assert.equal(buildTextExportFilename("../bad:name?.csv", { format: "csv", fallback: "table" }), "bad name.csv");
|
|
34
|
+
});
|