sqlite-hub 0.1.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.
Files changed (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,302 @@
1
+ const {
2
+ NotFoundError,
3
+ ValidationError,
4
+ mapSqliteError,
5
+ } = require("../../utils/errors");
6
+ const { quoteIdentifier } = require("../../utils/identifier");
7
+ const { normalizeDeclaredType } = require("../../utils/sqliteTypes");
8
+
9
+ function getTableListMap(db) {
10
+ try {
11
+ const rows = db.prepare("PRAGMA table_list").all();
12
+ return new Map(
13
+ rows
14
+ .filter((row) => row.schema === "main")
15
+ .map((row) => [row.name, row])
16
+ );
17
+ } catch (error) {
18
+ return new Map();
19
+ }
20
+ }
21
+
22
+ function getMasterEntry(db, type, name) {
23
+ const entry = db
24
+ .prepare(
25
+ "SELECT type, name, tbl_name AS tableName, sql FROM sqlite_master WHERE type = ? AND name = ?"
26
+ )
27
+ .get(type, name);
28
+
29
+ if (!entry) {
30
+ throw new NotFoundError(`${type} not found: ${name}`);
31
+ }
32
+
33
+ return entry;
34
+ }
35
+
36
+ function getRawStructureEntries(db) {
37
+ return db
38
+ .prepare(
39
+ [
40
+ "SELECT type, name, tbl_name AS tableName, sql",
41
+ "FROM sqlite_master",
42
+ "WHERE name NOT LIKE 'sqlite_%'",
43
+ "ORDER BY type ASC, name ASC",
44
+ ].join(" ")
45
+ )
46
+ .all();
47
+ }
48
+
49
+ function normalizeColumn(column, visibleSet) {
50
+ const typeMeta = normalizeDeclaredType(column.type);
51
+ const hiddenValue = Number(column.hidden ?? 0);
52
+
53
+ return {
54
+ cid: column.cid,
55
+ name: column.name,
56
+ declaredType: typeMeta.declaredType,
57
+ affinity: typeMeta.affinity,
58
+ notNull: Boolean(column.notnull),
59
+ defaultValue: column.dflt_value,
60
+ primaryKeyPosition: Number(column.pk ?? 0),
61
+ hidden: hiddenValue,
62
+ visible: visibleSet.has(column.name),
63
+ generated: hiddenValue === 2 || hiddenValue === 3,
64
+ };
65
+ }
66
+
67
+ function groupForeignKeys(rows) {
68
+ const grouped = new Map();
69
+
70
+ rows.forEach((row) => {
71
+ if (!grouped.has(row.id)) {
72
+ grouped.set(row.id, {
73
+ id: row.id,
74
+ referencedTable: row.table,
75
+ onUpdate: row.on_update,
76
+ onDelete: row.on_delete,
77
+ match: row.match,
78
+ mappings: [],
79
+ });
80
+ }
81
+
82
+ grouped.get(row.id).mappings.push({
83
+ sequence: row.seq,
84
+ from: row.from,
85
+ to: row.to,
86
+ });
87
+ });
88
+
89
+ return Array.from(grouped.values());
90
+ }
91
+
92
+ function safeCountRows(db, tableName) {
93
+ try {
94
+ const row = db
95
+ .prepare(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(tableName)}`)
96
+ .get();
97
+ return row?.count ?? 0;
98
+ } catch (error) {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ function resolveIdentityStrategy(tableDetail) {
104
+ const primaryKeyColumns = tableDetail.columns
105
+ .filter((column) => column.primaryKeyPosition > 0)
106
+ .sort((left, right) => left.primaryKeyPosition - right.primaryKeyPosition);
107
+
108
+ if (primaryKeyColumns.length > 0) {
109
+ return {
110
+ type: "primaryKey",
111
+ columns: primaryKeyColumns.map((column) => column.name),
112
+ composite: primaryKeyColumns.length > 1,
113
+ };
114
+ }
115
+
116
+ if (!tableDetail.withoutRowId) {
117
+ return {
118
+ type: "rowid",
119
+ columns: ["rowid"],
120
+ composite: false,
121
+ };
122
+ }
123
+
124
+ return {
125
+ type: "none",
126
+ columns: [],
127
+ composite: false,
128
+ };
129
+ }
130
+
131
+ function getTableDetail(db, tableName, options = {}) {
132
+ const entry = getMasterEntry(db, "table", tableName);
133
+ const tableList = getTableListMap(db);
134
+ const tableListEntry = tableList.get(tableName);
135
+ const tableInfo = db
136
+ .prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`)
137
+ .all();
138
+ const extendedInfo = db
139
+ .prepare(`PRAGMA table_xinfo(${quoteIdentifier(tableName)})`)
140
+ .all();
141
+ const visibleSet = new Set(tableInfo.map((column) => column.name));
142
+
143
+ const columns = extendedInfo
144
+ .map((column) => normalizeColumn(column, visibleSet))
145
+ .sort((left, right) => left.cid - right.cid);
146
+
147
+ const foreignKeys = groupForeignKeys(
148
+ db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
149
+ );
150
+
151
+ const indexList = db
152
+ .prepare(`PRAGMA index_list(${quoteIdentifier(tableName)})`)
153
+ .all()
154
+ .map((indexEntry) => {
155
+ let indexColumns = [];
156
+
157
+ try {
158
+ indexColumns = db
159
+ .prepare(`PRAGMA index_xinfo(${quoteIdentifier(indexEntry.name)})`)
160
+ .all()
161
+ .filter((row) => row.key === 1)
162
+ .map((row) => ({
163
+ sequence: row.seqno,
164
+ cid: row.cid,
165
+ name: row.name,
166
+ descending: Boolean(row.desc),
167
+ collation: row.coll,
168
+ }));
169
+ } catch (error) {
170
+ indexColumns = db
171
+ .prepare(`PRAGMA index_info(${quoteIdentifier(indexEntry.name)})`)
172
+ .all()
173
+ .map((row) => ({
174
+ sequence: row.seqno,
175
+ cid: row.cid,
176
+ name: row.name,
177
+ }));
178
+ }
179
+
180
+ return {
181
+ name: indexEntry.name,
182
+ unique: Boolean(indexEntry.unique),
183
+ origin: indexEntry.origin,
184
+ partial: Boolean(indexEntry.partial),
185
+ columns: indexColumns,
186
+ };
187
+ });
188
+
189
+ const triggers = db
190
+ .prepare(
191
+ "SELECT name, tbl_name AS tableName, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ? ORDER BY name ASC"
192
+ )
193
+ .all(tableName);
194
+
195
+ const withoutRowId =
196
+ typeof tableListEntry?.wr === "number"
197
+ ? Boolean(tableListEntry.wr)
198
+ : /WITHOUT\s+ROWID/i.test(entry.sql || "");
199
+
200
+ const tableDetail = {
201
+ type: entry.type,
202
+ name: entry.name,
203
+ ddl: entry.sql,
204
+ withoutRowId,
205
+ strict: Boolean(tableListEntry?.strict),
206
+ columns,
207
+ foreignKeys,
208
+ indexes: indexList,
209
+ indexCount: indexList.length,
210
+ triggers,
211
+ rowCount: options.includeRowCount === false ? null : safeCountRows(db, tableName),
212
+ };
213
+
214
+ tableDetail.identityStrategy = resolveIdentityStrategy(tableDetail);
215
+ tableDetail.notSafelyUpdatable = tableDetail.identityStrategy.type === "none";
216
+
217
+ return tableDetail;
218
+ }
219
+
220
+ function getViewDetail(db, viewName) {
221
+ const entry = getMasterEntry(db, "view", viewName);
222
+ let columns = [];
223
+
224
+ try {
225
+ columns = db
226
+ .prepare(`SELECT * FROM ${quoteIdentifier(viewName)} LIMIT 0`)
227
+ .columns()
228
+ .map((column) => ({
229
+ name: column.name,
230
+ }));
231
+ } catch (error) {
232
+ columns = [];
233
+ }
234
+
235
+ return {
236
+ type: entry.type,
237
+ name: entry.name,
238
+ ddl: entry.sql,
239
+ columns,
240
+ };
241
+ }
242
+
243
+ function getIndexDetail(db, indexName) {
244
+ const entry = getMasterEntry(db, "index", indexName);
245
+ const indexInfo = db
246
+ .prepare(`PRAGMA index_xinfo(${quoteIdentifier(indexName)})`)
247
+ .all()
248
+ .map((row) => ({
249
+ sequence: row.seqno,
250
+ cid: row.cid,
251
+ name: row.name,
252
+ descending: Boolean(row.desc),
253
+ collation: row.coll,
254
+ key: Boolean(row.key),
255
+ }));
256
+
257
+ return {
258
+ type: entry.type,
259
+ name: entry.name,
260
+ tableName: entry.tableName,
261
+ ddl: entry.sql,
262
+ columns: indexInfo,
263
+ };
264
+ }
265
+
266
+ function listSchema(db) {
267
+ const entries = getRawStructureEntries(db);
268
+ const tables = entries
269
+ .filter((entry) => entry.type === "table")
270
+ .map((entry) => getTableDetail(db, entry.name));
271
+ const views = entries
272
+ .filter((entry) => entry.type === "view")
273
+ .map((entry) => getViewDetail(db, entry.name));
274
+ const indexes = entries
275
+ .filter((entry) => entry.type === "index")
276
+ .map((entry) => getIndexDetail(db, entry.name));
277
+ const triggers = entries
278
+ .filter((entry) => entry.type === "trigger")
279
+ .map((entry) => ({
280
+ type: entry.type,
281
+ name: entry.name,
282
+ tableName: entry.tableName,
283
+ ddl: entry.sql,
284
+ }));
285
+
286
+ return {
287
+ tables,
288
+ views,
289
+ indexes,
290
+ triggers,
291
+ masterEntries: entries,
292
+ };
293
+ }
294
+
295
+ module.exports = {
296
+ getIndexDetail,
297
+ getRawStructureEntries,
298
+ getTableDetail,
299
+ getViewDetail,
300
+ listSchema,
301
+ resolveIdentityStrategy,
302
+ };
@@ -0,0 +1,109 @@
1
+ const { listSchema } = require("./introspection");
2
+
3
+ class OverviewService {
4
+ constructor({ connectionManager }) {
5
+ this.connectionManager = connectionManager;
6
+ }
7
+
8
+ safePragmaValue(db, pragmaName) {
9
+ const row = db.prepare(`PRAGMA ${pragmaName}`).get();
10
+ return row ? Object.values(row)[0] : null;
11
+ }
12
+
13
+ getDbStatSizes(db) {
14
+ try {
15
+ return db
16
+ .prepare(
17
+ [
18
+ "SELECT name, SUM(pgsize) AS sizeBytes",
19
+ "FROM dbstat",
20
+ "WHERE name NOT LIKE 'sqlite_%'",
21
+ "GROUP BY name",
22
+ "ORDER BY sizeBytes DESC",
23
+ ].join(" ")
24
+ )
25
+ .all();
26
+ } catch (error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ getOverview() {
32
+ const connection = this.connectionManager.getActiveConnection();
33
+ const db = this.connectionManager.getActiveDatabase();
34
+ const schema = listSchema(db);
35
+ const pageSize = this.safePragmaValue(db, "page_size");
36
+ const pageCount = this.safePragmaValue(db, "page_count");
37
+ const freelistCount = this.safePragmaValue(db, "freelist_count");
38
+ const warnings = [];
39
+
40
+ const dbStatSizes = this.getDbStatSizes(db);
41
+
42
+ if (!dbStatSizes) {
43
+ warnings.push("Estimated table sizes unavailable because dbstat is not enabled.");
44
+ }
45
+
46
+ return {
47
+ connection,
48
+ file: {
49
+ filename: connection.label,
50
+ path: connection.path,
51
+ sizeBytes: connection.sizeBytes,
52
+ lastModifiedAt: connection.lastModifiedAt,
53
+ },
54
+ sqlite: {
55
+ version: db.prepare("SELECT sqlite_version() AS version").get().version,
56
+ pageSize,
57
+ pageCount,
58
+ freelistCount,
59
+ journalMode: this.safePragmaValue(db, "journal_mode"),
60
+ foreignKeys: Boolean(this.safePragmaValue(db, "foreign_keys")),
61
+ autoVacuum: this.safePragmaValue(db, "auto_vacuum"),
62
+ encoding: this.safePragmaValue(db, "encoding"),
63
+ userVersion: this.safePragmaValue(db, "user_version"),
64
+ schemaVersion: this.safePragmaValue(db, "schema_version"),
65
+ integrityCheck: this.safePragmaValue(db, "integrity_check"),
66
+ quickCheck: this.safePragmaValue(db, "quick_check"),
67
+ },
68
+ counts: {
69
+ tables: schema.tables.length,
70
+ views: schema.views.length,
71
+ indexes: schema.indexes.length,
72
+ triggers: schema.triggers.length,
73
+ },
74
+ topTablesByRowCount: [...schema.tables]
75
+ .sort((left, right) => (right.rowCount ?? 0) - (left.rowCount ?? 0))
76
+ .slice(0, 10)
77
+ .map((table) => ({
78
+ name: table.name,
79
+ rowCount: table.rowCount,
80
+ indexCount: table.indexCount,
81
+ })),
82
+ topTablesByEstimatedSize: dbStatSizes
83
+ ? dbStatSizes.slice(0, 10).map((row) => ({
84
+ name: row.name,
85
+ sizeBytes: row.sizeBytes,
86
+ }))
87
+ : [],
88
+ estimatedSizeBytes:
89
+ typeof pageSize === "number" && typeof pageCount === "number"
90
+ ? pageSize * pageCount
91
+ : null,
92
+ warnings,
93
+ };
94
+ }
95
+
96
+ getStatus() {
97
+ const active = this.connectionManager.getActiveConnection();
98
+
99
+ return {
100
+ connected: Boolean(active),
101
+ activeConnection: active,
102
+ readOnly: active?.readOnly ?? false,
103
+ };
104
+ }
105
+ }
106
+
107
+ module.exports = {
108
+ OverviewService,
109
+ };