sqlite-hub 0.5.0 → 0.7.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.
Files changed (50) hide show
  1. package/README.md +109 -2
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +15 -0
  4. package/frontend/assets/mockups/connections.png +0 -0
  5. package/frontend/assets/mockups/data.png +0 -0
  6. package/frontend/assets/mockups/data_row_editor.png +0 -0
  7. package/frontend/assets/mockups/home.png +0 -0
  8. package/frontend/assets/mockups/sql_editor.png +0 -0
  9. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  10. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  11. package/frontend/assets/mockups/structure.png +0 -0
  12. package/frontend/assets/mockups/structure_inspector.png +0 -0
  13. package/frontend/index.html +1 -0
  14. package/frontend/js/api.js +49 -0
  15. package/frontend/js/app.js +377 -9
  16. package/frontend/js/components/modal.js +314 -1
  17. package/frontend/js/components/queryChartRenderer.js +125 -0
  18. package/frontend/js/components/queryEditor.js +37 -8
  19. package/frontend/js/components/queryHistoryPanel.js +16 -5
  20. package/frontend/js/components/sidebar.js +2 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +129 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/lib/queryChartOptions.js +283 -0
  25. package/frontend/js/lib/queryCharts.js +560 -0
  26. package/frontend/js/router.js +18 -0
  27. package/frontend/js/store.js +914 -1
  28. package/frontend/js/utils/tableDesigner.js +1192 -0
  29. package/frontend/js/views/charts.js +471 -0
  30. package/frontend/js/views/data.js +281 -277
  31. package/frontend/js/views/editor.js +19 -13
  32. package/frontend/js/views/structure.js +81 -39
  33. package/frontend/js/views/tableDesigner.js +37 -0
  34. package/frontend/styles/base.css +87 -73
  35. package/frontend/styles/components.css +790 -251
  36. package/frontend/styles/views.css +316 -46
  37. package/package.json +2 -1
  38. package/server/routes/charts.js +153 -0
  39. package/server/routes/tableDesigner.js +60 -0
  40. package/server/server.js +10 -0
  41. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  42. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  43. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  44. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  45. package/server/services/sqlite/tableDesignerService.js +181 -0
  46. package/server/services/storage/appStateStore.js +400 -1
  47. package/server/services/storage/queryHistoryChartUtils.js +145 -0
  48. package/frontend/assets/mockups/data_edit.png +0 -0
  49. package/frontend/assets/mockups/graph_visualize.png +0 -0
  50. package/frontend/assets/mockups/overview.png +0 -0
@@ -0,0 +1,245 @@
1
+ const { ValidationError } = require("../../../utils/errors");
2
+ const { assertValidIdentifier } = require("../../../utils/identifier");
3
+ const { normalizeDesignerType } = require("./schemaMapping");
4
+
5
+ function normalizeBoolean(value) {
6
+ return value === true || value === "true" || value === 1 || value === "1";
7
+ }
8
+
9
+ function normalizeSqlFragment(value) {
10
+ return String(value ?? "").trim();
11
+ }
12
+
13
+ function assertSafeSqlFragment(value, label, { allowEmpty = true } = {}) {
14
+ const normalized = normalizeSqlFragment(value);
15
+
16
+ if (!normalized) {
17
+ if (allowEmpty) {
18
+ return "";
19
+ }
20
+
21
+ throw new ValidationError(`${label} is required.`);
22
+ }
23
+
24
+ if (
25
+ normalized.includes("\0") ||
26
+ normalized.includes(";") ||
27
+ normalized.includes("--") ||
28
+ normalized.includes("/*") ||
29
+ normalized.includes("*/")
30
+ ) {
31
+ throw new ValidationError(
32
+ `${label} must be a single SQL fragment without statement separators or comments.`
33
+ );
34
+ }
35
+
36
+ return normalized;
37
+ }
38
+
39
+ function normalizeColumnPayload(column = {}, index = 0) {
40
+ const importedValueIndex =
41
+ column.importedValueIndex === null || column.importedValueIndex === undefined
42
+ ? null
43
+ : Number(column.importedValueIndex);
44
+
45
+ return {
46
+ id: String(column.id ?? `column:${index}`),
47
+ isNew: normalizeBoolean(column.isNew),
48
+ deleted: normalizeBoolean(column.deleted),
49
+ name: String(column.name ?? "").trim(),
50
+ type: normalizeDesignerType(column.type),
51
+ notNull: normalizeBoolean(column.notNull),
52
+ unique: normalizeBoolean(column.unique),
53
+ primaryKey: normalizeBoolean(column.primaryKey),
54
+ defaultValue: assertSafeSqlFragment(column.defaultValue, "Default value"),
55
+ referencesTable: String(column.referencesTable ?? "").trim(),
56
+ referencesColumn: String(column.referencesColumn ?? "").trim(),
57
+ originalName: String(column.originalName ?? "").trim(),
58
+ originalType: normalizeDesignerType(column.originalType || column.type),
59
+ originalNotNull: normalizeBoolean(column.originalNotNull),
60
+ originalUnique: normalizeBoolean(column.originalUnique),
61
+ originalPrimaryKey: normalizeBoolean(column.originalPrimaryKey),
62
+ originalDefaultValue: assertSafeSqlFragment(column.originalDefaultValue, "Original default value"),
63
+ originalReferencesTable: String(column.originalReferencesTable ?? "").trim(),
64
+ originalReferencesColumn: String(column.originalReferencesColumn ?? "").trim(),
65
+ importedValueIndex: Number.isInteger(importedValueIndex) ? importedValueIndex : null,
66
+ };
67
+ }
68
+
69
+ function normalizeDraftPayload(payload = {}) {
70
+ const mode = String(payload.mode ?? "create").trim() === "edit" ? "edit" : "create";
71
+
72
+ return {
73
+ mode,
74
+ originalTableName: String(payload.originalTableName ?? "").trim(),
75
+ tableName: String(payload.tableName ?? "").trim(),
76
+ columns: Array.isArray(payload.columns)
77
+ ? payload.columns.map((column, index) => normalizeColumnPayload(column, index))
78
+ : [],
79
+ schemaWarnings: Array.isArray(payload.schemaWarnings) ? payload.schemaWarnings : [],
80
+ warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
81
+ fillImportedRows: normalizeBoolean(payload.fillImportedRows),
82
+ importedCsvFileName: String(payload.importedCsvFileName ?? "").trim(),
83
+ importedCsvDelimiter: String(payload.importedCsvDelimiter ?? "").trim(),
84
+ importedCsvRows: Array.isArray(payload.importedCsvRows)
85
+ ? payload.importedCsvRows.map((row) =>
86
+ Array.isArray(row) ? row.map((cell) => String(cell ?? "")) : []
87
+ )
88
+ : [],
89
+ dirty: normalizeBoolean(payload.dirty),
90
+ };
91
+ }
92
+
93
+ function normalizeIdentifierKey(value) {
94
+ return String(value ?? "").trim().toLowerCase();
95
+ }
96
+
97
+ function buildSelfReferenceColumns(draft) {
98
+ return draft.columns
99
+ .filter((column) => !column.deleted)
100
+ .map((column) => column.name)
101
+ .filter(Boolean);
102
+ }
103
+
104
+ function validatePrimaryKeys(draft, originalDraft) {
105
+ const currentPrimaryKeyColumns = draft.columns.filter(
106
+ (column) => !column.deleted && column.primaryKey
107
+ );
108
+
109
+ if (currentPrimaryKeyColumns.length <= 1) {
110
+ return;
111
+ }
112
+
113
+ if (!originalDraft) {
114
+ throw new ValidationError(
115
+ "Table Designer v1 supports a single primary key column when creating a table."
116
+ );
117
+ }
118
+
119
+ const originalPrimaryKeyNames = new Set(
120
+ originalDraft.columns
121
+ .filter((column) => column.primaryKey)
122
+ .map((column) => normalizeIdentifierKey(column.originalName || column.name))
123
+ );
124
+ const currentPrimaryKeyNames = new Set(
125
+ currentPrimaryKeyColumns.map((column) =>
126
+ normalizeIdentifierKey(column.originalName || column.name)
127
+ )
128
+ );
129
+
130
+ const isUnchangedCompositePrimaryKey =
131
+ originalPrimaryKeyNames.size > 1 &&
132
+ originalPrimaryKeyNames.size === currentPrimaryKeyNames.size &&
133
+ [...currentPrimaryKeyNames].every((name) => originalPrimaryKeyNames.has(name));
134
+
135
+ if (!isUnchangedCompositePrimaryKey) {
136
+ throw new ValidationError(
137
+ "Table Designer v1 does not support editing composite primary keys."
138
+ );
139
+ }
140
+ }
141
+
142
+ function validateTableDesignerDraft(draft, { catalogTables = [], originalDraft = null } = {}) {
143
+ if (!["create", "edit"].includes(draft.mode)) {
144
+ throw new ValidationError("Draft mode must be create or edit.");
145
+ }
146
+
147
+ assertValidIdentifier(draft.tableName, "Table name");
148
+
149
+ if (draft.mode === "edit") {
150
+ assertValidIdentifier(draft.originalTableName, "Original table name");
151
+ }
152
+
153
+ if (!draft.columns.length) {
154
+ throw new ValidationError("At least one column is required.");
155
+ }
156
+
157
+ const activeColumns = draft.columns.filter((column) => !column.deleted);
158
+
159
+ if (!activeColumns.length) {
160
+ throw new ValidationError("At least one column is required.");
161
+ }
162
+
163
+ const catalogByName = new Map(
164
+ catalogTables.map((table) => [normalizeIdentifierKey(table.name), table])
165
+ );
166
+ const normalizedTableName = normalizeIdentifierKey(draft.tableName);
167
+ const normalizedOriginalTableName = normalizeIdentifierKey(draft.originalTableName);
168
+
169
+ if (
170
+ catalogByName.has(normalizedTableName) &&
171
+ (draft.mode === "create" || normalizedTableName !== normalizedOriginalTableName)
172
+ ) {
173
+ throw new ValidationError(`A table named ${draft.tableName} already exists.`);
174
+ }
175
+
176
+ const seenColumns = new Set();
177
+
178
+ activeColumns.forEach((column) => {
179
+ assertValidIdentifier(column.name, "Column name");
180
+ assertSafeSqlFragment(column.type, `Type for ${column.name}`, { allowEmpty: false });
181
+
182
+ const normalizedColumnName = normalizeIdentifierKey(column.name);
183
+
184
+ if (seenColumns.has(normalizedColumnName)) {
185
+ throw new ValidationError(`Duplicate column name: ${column.name}`);
186
+ }
187
+
188
+ seenColumns.add(normalizedColumnName);
189
+
190
+ const hasReferenceTable = Boolean(column.referencesTable);
191
+ const hasReferenceColumn = Boolean(column.referencesColumn);
192
+
193
+ if (hasReferenceTable !== hasReferenceColumn) {
194
+ throw new ValidationError(
195
+ `Column ${column.name} must define both a referenced table and a referenced column.`
196
+ );
197
+ }
198
+
199
+ if (!hasReferenceTable || !hasReferenceColumn) {
200
+ return;
201
+ }
202
+
203
+ const normalizedReferenceTable = normalizeIdentifierKey(column.referencesTable);
204
+ const referencedTable =
205
+ normalizedReferenceTable === normalizedTableName
206
+ ? {
207
+ name: draft.tableName,
208
+ columns: buildSelfReferenceColumns(draft),
209
+ }
210
+ : catalogByName.get(normalizedReferenceTable);
211
+
212
+ if (!referencedTable) {
213
+ throw new ValidationError(
214
+ `Referenced table ${column.referencesTable} does not exist in the current SQLite schema.`
215
+ );
216
+ }
217
+
218
+ const hasReferencedColumn = (referencedTable.columns ?? []).some(
219
+ (candidate) => normalizeIdentifierKey(candidate) === normalizeIdentifierKey(column.referencesColumn)
220
+ );
221
+
222
+ if (!hasReferencedColumn) {
223
+ throw new ValidationError(
224
+ `Referenced column ${column.referencesColumn} does not exist on ${referencedTable.name}.`
225
+ );
226
+ }
227
+ });
228
+
229
+ validatePrimaryKeys(draft, originalDraft);
230
+
231
+ if (draft.mode === "edit" && draft.fillImportedRows) {
232
+ throw new ValidationError("Imported row fill is only available when creating a table.");
233
+ }
234
+
235
+ if (draft.fillImportedRows && !draft.importedCsvRows.length) {
236
+ throw new ValidationError("Fill requires imported CSV rows.");
237
+ }
238
+ }
239
+
240
+ module.exports = {
241
+ normalizeDraftPayload,
242
+ normalizeIdentifierKey,
243
+ normalizeSqlFragment,
244
+ validateTableDesignerDraft,
245
+ };
@@ -0,0 +1,181 @@
1
+ const { mapSqliteError, ValidationError } = require("../../utils/errors");
2
+ const { getTableDetail } = require("./introspection");
3
+ const { analyzeTableDesignerChanges } = require("./tableDesigner/changeAnalysis");
4
+ const {
5
+ SUPPORTED_TABLE_DESIGNER_TYPES,
6
+ buildTableDesignerDraft,
7
+ listDesignerTables,
8
+ normalizeDesignerType,
9
+ } = require("./tableDesigner/schemaMapping");
10
+ const {
11
+ normalizeDraftPayload,
12
+ validateTableDesignerDraft,
13
+ } = require("./tableDesigner/validation");
14
+ const { buildCreateTableSql, buildInsertRowsSql } = require("./tableDesigner/sql");
15
+
16
+ function getImportedFillColumns(draft) {
17
+ return draft.columns.filter(
18
+ (column) => !column.deleted && Number.isInteger(column.importedValueIndex)
19
+ );
20
+ }
21
+
22
+ function coerceImportedCellValue(column, value) {
23
+ const textValue = String(value ?? "");
24
+ const trimmedValue = textValue.trim();
25
+ const type = normalizeDesignerType(column.type);
26
+
27
+ if (!trimmedValue) {
28
+ return ["TEXT", "DATE", "DATETIME"].includes(type) ? "" : null;
29
+ }
30
+
31
+ if (type === "BOOLEAN") {
32
+ const normalized = trimmedValue.toLowerCase();
33
+
34
+ if (["true", "yes", "1"].includes(normalized)) {
35
+ return 1;
36
+ }
37
+
38
+ if (["false", "no", "0"].includes(normalized)) {
39
+ return 0;
40
+ }
41
+ }
42
+
43
+ if (type === "INTEGER" && /^-?\d+$/.test(trimmedValue)) {
44
+ return Number(trimmedValue);
45
+ }
46
+
47
+ if (
48
+ ["REAL", "NUMERIC"].includes(type) &&
49
+ /^-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(trimmedValue)
50
+ ) {
51
+ return Number(trimmedValue);
52
+ }
53
+
54
+ return textValue;
55
+ }
56
+
57
+ class TableDesignerService {
58
+ constructor({ connectionManager }) {
59
+ this.connectionManager = connectionManager;
60
+ }
61
+
62
+ getOverview() {
63
+ const db = this.connectionManager.getActiveDatabase();
64
+
65
+ return {
66
+ tables: listDesignerTables(db),
67
+ supportedTypes: SUPPORTED_TABLE_DESIGNER_TYPES,
68
+ };
69
+ }
70
+
71
+ getTableDraft(tableName) {
72
+ const db = this.connectionManager.getActiveDatabase();
73
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
74
+
75
+ return {
76
+ draft: buildTableDesignerDraft(tableDetail),
77
+ supportedTypes: SUPPORTED_TABLE_DESIGNER_TYPES,
78
+ };
79
+ }
80
+
81
+ saveDraft(payload = {}) {
82
+ this.connectionManager.assertWritable();
83
+
84
+ const db = this.connectionManager.getActiveDatabase();
85
+ const draft = normalizeDraftPayload(payload.draft ?? payload);
86
+ const catalogTables = listDesignerTables(db);
87
+ const originalDraft =
88
+ draft.mode === "edit"
89
+ ? buildTableDesignerDraft(
90
+ getTableDetail(db, draft.originalTableName, { includeRowCount: false })
91
+ )
92
+ : null;
93
+
94
+ validateTableDesignerDraft(draft, {
95
+ catalogTables,
96
+ originalDraft,
97
+ });
98
+
99
+ const analysis = analyzeTableDesignerChanges({ draft, originalDraft });
100
+
101
+ if (!analysis.dirty) {
102
+ return {
103
+ savedTableName: draft.mode === "edit" ? draft.originalTableName : draft.tableName,
104
+ executedSql: [],
105
+ draft: originalDraft ?? draft,
106
+ tables: catalogTables,
107
+ };
108
+ }
109
+
110
+ if (!analysis.executable) {
111
+ throw new ValidationError(
112
+ "This schema change set would require a SQLite table rebuild. Table Designer v1 keeps the SQL preview available but will not execute those changes automatically.",
113
+ {
114
+ code: "TABLE_DESIGNER_REBUILD_REQUIRED",
115
+ warnings: analysis.warnings,
116
+ }
117
+ );
118
+ }
119
+
120
+ const executeStatements = db.transaction((statements) => {
121
+ statements.forEach((statement) => {
122
+ db.exec(statement);
123
+ });
124
+ });
125
+
126
+ const executeCreateWithImport = db.transaction((nextDraft) => {
127
+ db.exec(buildCreateTableSql(nextDraft));
128
+
129
+ if (!nextDraft.fillImportedRows || !nextDraft.importedCsvRows.length) {
130
+ return;
131
+ }
132
+
133
+ const importedColumns = getImportedFillColumns(nextDraft);
134
+
135
+ if (!importedColumns.length) {
136
+ return;
137
+ }
138
+
139
+ const insertStatement = db.prepare(
140
+ buildInsertRowsSql(
141
+ nextDraft.tableName,
142
+ importedColumns.map((column) => column.name)
143
+ )
144
+ );
145
+
146
+ nextDraft.importedCsvRows.forEach((row) => {
147
+ insertStatement.run(
148
+ importedColumns.map((column) =>
149
+ coerceImportedCellValue(column, row[column.importedValueIndex] ?? "")
150
+ )
151
+ );
152
+ });
153
+ });
154
+
155
+ try {
156
+ if (draft.mode === "create") {
157
+ executeCreateWithImport(draft);
158
+ } else {
159
+ executeStatements(analysis.statements);
160
+ }
161
+ } catch (error) {
162
+ throw mapSqliteError(error);
163
+ }
164
+
165
+ const savedTableName = draft.tableName;
166
+ const nextDraft = buildTableDesignerDraft(
167
+ getTableDetail(db, savedTableName, { includeRowCount: false })
168
+ );
169
+
170
+ return {
171
+ savedTableName,
172
+ executedSql: analysis.statements,
173
+ draft: nextDraft,
174
+ tables: listDesignerTables(db),
175
+ };
176
+ }
177
+ }
178
+
179
+ module.exports = {
180
+ TableDesignerService,
181
+ };