sqlite-hub 1.2.0 → 1.5.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 (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
@@ -0,0 +1,663 @@
1
+ const path = require("node:path");
2
+ const { NotFoundError, ValidationError } = require("../utils/errors");
3
+ const { quoteIdentifier } = require("../utils/identifier");
4
+ const { normalizeDeclaredType } = require("../utils/sqliteTypes");
5
+ const { getTableDetail } = require("./sqlite/introspection");
6
+
7
+ const TARGETS = new Set(["typescript", "rust", "kotlin", "swift"]);
8
+ const TARGET_ALIASES = {
9
+ ts: "typescript",
10
+ typescript: "typescript",
11
+ rs: "rust",
12
+ rust: "rust",
13
+ kt: "kotlin",
14
+ kotlin: "kotlin",
15
+ swift: "swift",
16
+ };
17
+ const FILE_EXTENSIONS = {
18
+ typescript: ".ts",
19
+ rust: ".rs",
20
+ kotlin: ".kt",
21
+ swift: ".swift",
22
+ };
23
+ const DEFAULT_NAMING = {
24
+ typescript: "camel",
25
+ rust: "snake",
26
+ kotlin: "camel",
27
+ swift: "camel",
28
+ };
29
+ const PROPERTY_NAMING = new Set(["preserve", "camel", "pascal", "snake"]);
30
+ const NULLABLE_MODES = new Set(["native", "optional"]);
31
+ const JSON_TYPES = new Set(["unknown", "record", "json-value"]);
32
+
33
+ function normalizeTarget(value, { allowAliases = true } = {}) {
34
+ const normalized = String(value ?? "").trim().toLowerCase();
35
+ const target = allowAliases ? TARGET_ALIASES[normalized] : normalized;
36
+
37
+ if (!TARGETS.has(target)) {
38
+ throw new ValidationError(
39
+ `Unsupported type target "${value}". Supported targets: typescript, rust, kotlin, swift.`,
40
+ { code: "INVALID_TYPE_TARGET" }
41
+ );
42
+ }
43
+
44
+ return target;
45
+ }
46
+
47
+ function splitWords(value) {
48
+ const text = String(value ?? "")
49
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
50
+ .replace(/[^A-Za-z0-9]+/g, " ")
51
+ .trim();
52
+
53
+ return text ? text.split(/\s+/) : ["value"];
54
+ }
55
+
56
+ function toPascalCase(value) {
57
+ return splitWords(value)
58
+ .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`)
59
+ .join("") || "Value";
60
+ }
61
+
62
+ function toCamelCase(value) {
63
+ const pascal = toPascalCase(value);
64
+ return `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`;
65
+ }
66
+
67
+ function toSnakeCase(value) {
68
+ return splitWords(value).map((word) => word.toLowerCase()).join("_") || "value";
69
+ }
70
+
71
+ function singularizeTableName(name) {
72
+ const text = String(name ?? "").trim();
73
+
74
+ if (/ies$/i.test(text) && text.length > 4) {
75
+ return `${text.slice(0, -3)}y`;
76
+ }
77
+
78
+ if (/ses$/i.test(text) || /xes$/i.test(text) || /ches$/i.test(text) || /shes$/i.test(text)) {
79
+ return text.slice(0, -2);
80
+ }
81
+
82
+ if (/s$/i.test(text) && !/ss$/i.test(text) && text.length > 3) {
83
+ return text.slice(0, -1);
84
+ }
85
+
86
+ return text || "GeneratedType";
87
+ }
88
+
89
+ function transformPropertyName(name, mode) {
90
+ if (mode === "preserve") return String(name);
91
+ if (mode === "pascal") return toPascalCase(name);
92
+ if (mode === "snake") return toSnakeCase(name);
93
+ return toCamelCase(name);
94
+ }
95
+
96
+ function validateTypeName(typeName) {
97
+ const normalized = String(typeName ?? "").trim();
98
+
99
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) {
100
+ throw new ValidationError(`Invalid type name "${typeName}".`, { code: "INVALID_TYPE_NAME" });
101
+ }
102
+
103
+ return normalized;
104
+ }
105
+
106
+ function normalizeOptions(target, options = {}) {
107
+ const allowed = new Set([
108
+ "typeName",
109
+ "propertyNaming",
110
+ "nullableMode",
111
+ "includeComments",
112
+ "includeDefaultsAsComments",
113
+ "includeGeneratedColumns",
114
+ "includeHiddenColumns",
115
+ "exportDeclaration",
116
+ "jsonType",
117
+ ]);
118
+ const unknown = Object.keys(options ?? {}).filter((key) => !allowed.has(key));
119
+
120
+ if (unknown.length) {
121
+ throw new ValidationError(`Unknown type generation options: ${unknown.join(", ")}.`, {
122
+ code: "INVALID_TYPE_OPTIONS",
123
+ details: { unknown },
124
+ });
125
+ }
126
+
127
+ const propertyNaming = options.propertyNaming ?? DEFAULT_NAMING[target];
128
+ const nullableMode = options.nullableMode ?? "native";
129
+ const jsonType = options.jsonType ?? "unknown";
130
+
131
+ if (!PROPERTY_NAMING.has(propertyNaming)) {
132
+ throw new ValidationError(`Unsupported property naming "${propertyNaming}".`, {
133
+ code: "INVALID_TYPE_OPTIONS",
134
+ });
135
+ }
136
+
137
+ if (!NULLABLE_MODES.has(nullableMode) || (nullableMode === "optional" && target !== "typescript")) {
138
+ throw new ValidationError(`Unsupported nullable mode "${nullableMode}" for ${target}.`, {
139
+ code: "INVALID_TYPE_OPTIONS",
140
+ });
141
+ }
142
+
143
+ if (!JSON_TYPES.has(jsonType) || (options.jsonType && target !== "typescript")) {
144
+ throw new ValidationError(`Unsupported JSON type option "${jsonType}" for ${target}.`, {
145
+ code: "INVALID_TYPE_OPTIONS",
146
+ });
147
+ }
148
+
149
+ return {
150
+ typeName: options.typeName ? validateTypeName(options.typeName) : null,
151
+ propertyNaming,
152
+ nullableMode,
153
+ includeComments: Boolean(options.includeComments),
154
+ includeDefaultsAsComments: Boolean(options.includeDefaultsAsComments),
155
+ includeGeneratedColumns: options.includeGeneratedColumns !== false,
156
+ includeHiddenColumns: Boolean(options.includeHiddenColumns),
157
+ exportDeclaration: options.exportDeclaration !== false,
158
+ jsonType,
159
+ };
160
+ }
161
+
162
+ function skipQuotedSql(text, index) {
163
+ const quote = text[index];
164
+ let cursor = index + 1;
165
+
166
+ while (cursor < text.length) {
167
+ if (text[cursor] === quote) {
168
+ if (text[cursor + 1] === quote) {
169
+ cursor += 2;
170
+ continue;
171
+ }
172
+ return cursor + 1;
173
+ }
174
+ cursor += 1;
175
+ }
176
+
177
+ return text.length;
178
+ }
179
+
180
+ function findMatchingParenthesis(text, openIndex) {
181
+ let depth = 0;
182
+
183
+ for (let index = openIndex; index < text.length; index += 1) {
184
+ const char = text[index];
185
+ if (char === "'" || char === '"' || char === "`") {
186
+ index = skipQuotedSql(text, index) - 1;
187
+ continue;
188
+ }
189
+ if (char === "[") {
190
+ const closeIndex = text.indexOf("]", index + 1);
191
+ index = closeIndex === -1 ? text.length : closeIndex;
192
+ continue;
193
+ }
194
+ if (char === "(") depth += 1;
195
+ if (char === ")") {
196
+ depth -= 1;
197
+ if (depth === 0) return index;
198
+ }
199
+ }
200
+
201
+ return -1;
202
+ }
203
+
204
+ function extractCheckExpressions(sql = "") {
205
+ const expressions = [];
206
+ const pattern = /\bCHECK\s*\(/gi;
207
+ let match;
208
+
209
+ while ((match = pattern.exec(sql))) {
210
+ const openIndex = sql.indexOf("(", match.index);
211
+ const closeIndex = findMatchingParenthesis(sql, openIndex);
212
+ if (closeIndex === -1) continue;
213
+ expressions.push(sql.slice(openIndex + 1, closeIndex).trim());
214
+ pattern.lastIndex = closeIndex + 1;
215
+ }
216
+
217
+ return expressions;
218
+ }
219
+
220
+ function normalizeSqlIdentifier(value) {
221
+ const text = String(value ?? "").trim();
222
+ if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1).replace(/""/g, '"');
223
+ if (text.startsWith("`") && text.endsWith("`")) return text.slice(1, -1).replace(/``/g, "`");
224
+ if (text.startsWith("[") && text.endsWith("]")) return text.slice(1, -1).replace(/\]\]/g, "]");
225
+ return text;
226
+ }
227
+
228
+ function parseSqlValueList(text = "") {
229
+ const values = [];
230
+ let index = 0;
231
+ let sawString = false;
232
+ let sawNumber = false;
233
+
234
+ while (index < text.length) {
235
+ while (/\s|,/.test(text[index] ?? "")) index += 1;
236
+ if (index >= text.length) break;
237
+
238
+ if (text[index] === "'") {
239
+ sawString = true;
240
+ let value = "";
241
+ index += 1;
242
+ while (index < text.length) {
243
+ if (text[index] === "'") {
244
+ if (text[index + 1] === "'") {
245
+ value += "'";
246
+ index += 2;
247
+ continue;
248
+ }
249
+ index += 1;
250
+ values.push(value);
251
+ break;
252
+ }
253
+ value += text[index];
254
+ index += 1;
255
+ }
256
+ continue;
257
+ }
258
+
259
+ const match = text.slice(index).match(/^[-+]?(?:\d+|\d*\.\d+)$/);
260
+ const tokenMatch = text.slice(index).match(/^[-+]?(?:\d+|\d*\.\d+)/);
261
+ if (!tokenMatch) return { values: [], mixed: false, supported: false };
262
+ const token = tokenMatch[0];
263
+ if (!match && /[A-Za-z_]/.test(text[index + token.length] ?? "")) {
264
+ return { values: [], mixed: false, supported: false };
265
+ }
266
+ sawNumber = true;
267
+ values.push(token.includes(".") ? Number(token) : Number.parseInt(token, 10));
268
+ index += token.length;
269
+ }
270
+
271
+ return { values, mixed: sawString && sawNumber, supported: true };
272
+ }
273
+
274
+ function parseCheckInExpressions(sql = "", columns = []) {
275
+ const columnMap = new Map(columns.map((column) => [String(column.name).toLowerCase(), column.name]));
276
+ const matchesByColumn = new Map();
277
+ const expressions = extractCheckExpressions(sql);
278
+
279
+ for (const expression of expressions) {
280
+ const pattern = /(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)\s+IN\s*\(/gi;
281
+ let match;
282
+ while ((match = pattern.exec(expression))) {
283
+ const rawIdentifier = match[0].replace(/\s+IN\s*\($/i, "").trim();
284
+ const columnName = columnMap.get(normalizeSqlIdentifier(rawIdentifier).toLowerCase());
285
+ if (!columnName) continue;
286
+ const openIndex = expression.indexOf("(", match.index + rawIdentifier.length);
287
+ const closeIndex = findMatchingParenthesis(expression, openIndex);
288
+ if (closeIndex === -1) continue;
289
+ const parsed = parseSqlValueList(expression.slice(openIndex + 1, closeIndex));
290
+ const entry = {
291
+ expression,
292
+ values: parsed.supported && !parsed.mixed ? parsed.values : null,
293
+ supported: parsed.supported && !parsed.mixed && parsed.values.length > 0,
294
+ };
295
+ if (!matchesByColumn.has(columnName)) matchesByColumn.set(columnName, []);
296
+ matchesByColumn.get(columnName).push(entry);
297
+ }
298
+ }
299
+
300
+ return { expressions, matchesByColumn };
301
+ }
302
+
303
+ function normalizeColumnType(column, checkMatches = []) {
304
+ const declared = String(column.declaredType ?? "").trim().toUpperCase();
305
+ const affinity = String(column.affinity ?? normalizeDeclaredType(declared).affinity).toUpperCase();
306
+ const singleCheck = checkMatches.length === 1 ? checkMatches[0] : null;
307
+
308
+ if (["BOOLEAN", "BOOL"].some((type) => declared.includes(type))) return "boolean";
309
+ if (
310
+ singleCheck?.supported &&
311
+ ["INTEGER", "NUMERIC"].includes(affinity) &&
312
+ singleCheck.values.length === 2 &&
313
+ singleCheck.values.includes(0) &&
314
+ singleCheck.values.includes(1)
315
+ ) {
316
+ return "boolean";
317
+ }
318
+ if (declared.includes("JSON")) return "json";
319
+ if (declared.includes("DATETIME") || declared.includes("TIMESTAMP")) return "datetime";
320
+ if (declared === "DATE" || /\bDATE\b/.test(declared)) return "date";
321
+ if (affinity === "INTEGER") return "integer";
322
+ if (affinity === "REAL") return "real";
323
+ if (affinity === "NUMERIC") return declared ? "numeric" : "unknown";
324
+ if (affinity === "TEXT") return "text";
325
+ if (affinity === "BLOB") return declared ? "blob" : "unknown";
326
+ return "unknown";
327
+ }
328
+
329
+ function isColumnNullable(column, tableDetail) {
330
+ if (column.notNull) return false;
331
+ if (column.primaryKeyPosition > 0) {
332
+ const declared = String(column.declaredType ?? "").toUpperCase();
333
+ const primaryKeyColumns = tableDetail.columns.filter((candidate) => candidate.primaryKeyPosition > 0);
334
+ return !(primaryKeyColumns.length === 1 && declared.includes("INT"));
335
+ }
336
+ return true;
337
+ }
338
+
339
+ function buildComments(column, options) {
340
+ if (!options.includeComments && !options.includeDefaultsAsComments) return [];
341
+ const comments = [];
342
+ if (options.includeComments) {
343
+ if (column.primaryKey) comments.push("Primary key");
344
+ if (column.foreignKey) comments.push(`References ${column.foreignKey.table}.${column.foreignKey.column ?? "id"}`);
345
+ if (column.generated) comments.push("Generated column");
346
+ if (column.declaredType) comments.push(`SQLite type: ${column.declaredType}`);
347
+ }
348
+ if (options.includeDefaultsAsComments && column.defaultValue !== null && column.defaultValue !== undefined) {
349
+ comments.push(`Default: ${column.defaultValue}`);
350
+ }
351
+ return comments;
352
+ }
353
+
354
+ function commentBlock(comments, prefix = " ") {
355
+ if (!comments.length) return [];
356
+ if (comments.length === 1) return [`${prefix}/** ${comments[0].replace(/\*\//g, "* /")} */`];
357
+ return [
358
+ `${prefix}/**`,
359
+ ...comments.map((comment) => `${prefix} * ${comment.replace(/\*\//g, "* /")}`),
360
+ `${prefix} */`,
361
+ ];
362
+ }
363
+
364
+ function quoteTsProperty(name) {
365
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
366
+ }
367
+
368
+ function tsType(column, options) {
369
+ if (column.allowedValues?.length) {
370
+ return column.allowedValues.map((value) => (typeof value === "string" ? JSON.stringify(value) : String(value))).join(" | ");
371
+ }
372
+ if (column.normalizedType === "integer" || column.normalizedType === "real" || column.normalizedType === "numeric") return "number";
373
+ if (column.normalizedType === "boolean") return "boolean";
374
+ if (column.normalizedType === "blob") return "Uint8Array";
375
+ if (column.normalizedType === "json") {
376
+ if (options.jsonType === "record") return "Record<string, unknown>";
377
+ if (options.jsonType === "json-value") return "JsonValue";
378
+ return "unknown";
379
+ }
380
+ if (column.normalizedType === "unknown") return "unknown";
381
+ return "string";
382
+ }
383
+
384
+ function generateTypeScript(table, options) {
385
+ const lines = [];
386
+ if (options.jsonType === "json-value" && table.columns.some((column) => column.normalizedType === "json")) {
387
+ lines.push("export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };", "");
388
+ }
389
+ lines.push(`${options.exportDeclaration ? "export " : ""}interface ${table.suggestedTypeName} {`);
390
+ for (const column of table.columns) {
391
+ lines.push(...commentBlock(buildComments(column, options), " "));
392
+ const optional = column.nullable && options.nullableMode === "optional" ? "?" : "";
393
+ const nullable = column.nullable && options.nullableMode === "native" ? " | null" : "";
394
+ lines.push(` ${quoteTsProperty(column.propertyName)}${optional}: ${tsType(column, options)}${nullable};`);
395
+ if (options.includeComments || options.includeDefaultsAsComments) lines.push("");
396
+ }
397
+ if (lines[lines.length - 1] === "") lines.pop();
398
+ lines.push("}");
399
+ return lines.join("\n");
400
+ }
401
+
402
+ const RUST_KEYWORDS = new Set(["type", "struct", "enum", "crate", "self", "Self", "super", "mod", "pub", "use", "where", "fn", "let", "match"]);
403
+ const KOTLIN_KEYWORDS = new Set(["when", "class", "object", "interface", "val", "var", "fun", "is", "in", "as", "typealias"]);
404
+ const SWIFT_KEYWORDS = new Set(["protocol", "class", "struct", "enum", "let", "var", "func", "import", "switch", "case", "default"]);
405
+
406
+ function enumName(typeName, propertyName) {
407
+ return `${typeName}${toPascalCase(propertyName)}`;
408
+ }
409
+
410
+ function rustFieldName(name) {
411
+ return RUST_KEYWORDS.has(name) ? `r#${name}` : name;
412
+ }
413
+
414
+ function rustType(column) {
415
+ if (column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")) return enumName("", column.propertyName).replace(/^\w/, (c) => c.toUpperCase());
416
+ if (column.normalizedType === "integer") return "i64";
417
+ if (["real", "numeric"].includes(column.normalizedType)) return "f64";
418
+ if (column.normalizedType === "boolean") return "bool";
419
+ if (column.normalizedType === "blob") return "Vec<u8>";
420
+ if (["json", "unknown"].includes(column.normalizedType)) return "serde_json::Value";
421
+ return "String";
422
+ }
423
+
424
+ function generateRust(table, options) {
425
+ const lines = ["use serde::{Deserialize, Serialize};"];
426
+ if (table.columns.some((column) => ["json", "unknown"].includes(column.normalizedType))) lines.push("use serde_json::Value;");
427
+ lines.push("");
428
+ for (const column of table.columns.filter((candidate) => candidate.allowedValues?.length && candidate.allowedValues.every((value) => typeof value === "string"))) {
429
+ const name = enumName(table.suggestedTypeName, column.propertyName);
430
+ lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]", `pub enum ${name} {`);
431
+ for (const value of column.allowedValues) {
432
+ lines.push(` #[serde(rename = ${JSON.stringify(value)})]`, ` ${toPascalCase(value)},`);
433
+ }
434
+ lines.push("}", "");
435
+ }
436
+ lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]", `pub struct ${table.suggestedTypeName} {`);
437
+ for (const column of table.columns) {
438
+ const fieldName = rustFieldName(column.propertyName);
439
+ const baseType =
440
+ column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")
441
+ ? enumName(table.suggestedTypeName, column.propertyName)
442
+ : rustType(column);
443
+ if (column.propertyName !== column.databaseName) lines.push(` #[serde(rename = ${JSON.stringify(column.databaseName)})]`);
444
+ lines.push(` pub ${fieldName}: ${column.nullable ? `Option<${baseType}>` : baseType},`);
445
+ }
446
+ lines.push("}");
447
+ return lines.join("\n");
448
+ }
449
+
450
+ function kotlinFieldName(name) {
451
+ return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
452
+ }
453
+
454
+ function kotlinType(column) {
455
+ if (column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")) return enumName("", column.propertyName).replace(/^\w/, (c) => c.toUpperCase());
456
+ if (column.normalizedType === "integer") return "Long";
457
+ if (["real", "numeric"].includes(column.normalizedType)) return "Double";
458
+ if (column.normalizedType === "boolean") return "Boolean";
459
+ if (column.normalizedType === "blob") return "ByteArray";
460
+ if (column.normalizedType === "json") return "JsonElement";
461
+ if (column.normalizedType === "unknown") return "Any";
462
+ return "String";
463
+ }
464
+
465
+ function generateKotlin(table) {
466
+ const lines = [];
467
+ for (const column of table.columns.filter((candidate) => candidate.allowedValues?.length && candidate.allowedValues.every((value) => typeof value === "string"))) {
468
+ lines.push(`enum class ${enumName(table.suggestedTypeName, column.propertyName)} {`);
469
+ lines.push(...column.allowedValues.map((value) => ` ${toSnakeCase(value).toUpperCase()},`));
470
+ lines.push("}", "");
471
+ }
472
+ lines.push(`data class ${table.suggestedTypeName}(`);
473
+ table.columns.forEach((column, index) => {
474
+ const baseType =
475
+ column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")
476
+ ? enumName(table.suggestedTypeName, column.propertyName)
477
+ : kotlinType(column);
478
+ const comma = index === table.columns.length - 1 ? "" : ",";
479
+ lines.push(` val ${kotlinFieldName(column.propertyName)}: ${baseType}${column.nullable ? "?" : ""}${comma}`);
480
+ });
481
+ lines.push(")");
482
+ return lines.join("\n");
483
+ }
484
+
485
+ function swiftFieldName(name) {
486
+ return SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
487
+ }
488
+
489
+ function swiftType(column) {
490
+ if (column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")) return enumName("", column.propertyName).replace(/^\w/, (c) => c.toUpperCase());
491
+ if (column.normalizedType === "integer") return "Int64";
492
+ if (["real", "numeric"].includes(column.normalizedType)) return "Double";
493
+ if (column.normalizedType === "boolean") return "Bool";
494
+ if (column.normalizedType === "blob") return "Data";
495
+ if (column.normalizedType === "json") return "JSONValue";
496
+ if (column.normalizedType === "unknown") return "AnyCodable";
497
+ return "String";
498
+ }
499
+
500
+ function generateSwift(table) {
501
+ const needsFoundation = table.columns.some((column) => column.normalizedType === "blob");
502
+ const lines = needsFoundation ? ["import Foundation", ""] : [];
503
+ for (const column of table.columns.filter((candidate) => candidate.allowedValues?.length && candidate.allowedValues.every((value) => typeof value === "string"))) {
504
+ lines.push(`enum ${enumName(table.suggestedTypeName, column.propertyName)}: String, Codable {`);
505
+ for (const value of column.allowedValues) lines.push(` case ${toCamelCase(value)} = ${JSON.stringify(value)}`);
506
+ lines.push("}", "");
507
+ }
508
+ lines.push(`struct ${table.suggestedTypeName}: Codable {`);
509
+ for (const column of table.columns) {
510
+ const baseType =
511
+ column.allowedValues?.length && column.allowedValues.every((value) => typeof value === "string")
512
+ ? enumName(table.suggestedTypeName, column.propertyName)
513
+ : swiftType(column);
514
+ lines.push(` let ${swiftFieldName(column.propertyName)}: ${baseType}${column.nullable ? "?" : ""}`);
515
+ }
516
+ const renamed = table.columns.filter((column) => column.propertyName !== column.databaseName);
517
+ if (renamed.length) {
518
+ lines.push("", " enum CodingKeys: String, CodingKey {");
519
+ for (const column of table.columns) {
520
+ lines.push(` case ${swiftFieldName(column.propertyName)}${column.propertyName === column.databaseName ? "" : ` = ${JSON.stringify(column.databaseName)}`}`);
521
+ }
522
+ lines.push(" }");
523
+ }
524
+ lines.push("}");
525
+ return lines.join("\n");
526
+ }
527
+
528
+ function makeResult(target, table, options, code, warnings, metadata) {
529
+ const fileName = `${table.suggestedTypeName}${FILE_EXTENSIONS[target]}`;
530
+ return {
531
+ target,
532
+ language: target,
533
+ tableName: table.tableName,
534
+ typeName: table.suggestedTypeName,
535
+ fileName,
536
+ code,
537
+ warnings,
538
+ metadata,
539
+ };
540
+ }
541
+
542
+ class TypeGenerationService {
543
+ generateTypesFromDatabase(db, tableName, targetInput, optionInput = {}) {
544
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
545
+ const ddl = tableDetail.ddl ?? db
546
+ .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ?")
547
+ .get(tableName)?.sql ?? null;
548
+
549
+ return this.generateTypesFromTableDetail(tableDetail, targetInput, optionInput, ddl);
550
+ }
551
+
552
+ generateTypesFromTableDetail(tableDetail, targetInput, optionInput = {}, ddlOverride = undefined) {
553
+ const target = normalizeTarget(targetInput, { allowAliases: true });
554
+ const options = normalizeOptions(target, optionInput);
555
+ const ddl = ddlOverride === undefined ? tableDetail.ddl : ddlOverride;
556
+ const checkAnalysis = parseCheckInExpressions(ddl ?? "", tableDetail.columns ?? []);
557
+ const warnings = ["SQLite uses dynamic typing. Generated types are based on declared column types and schema constraints."];
558
+ const typeName = options.typeName ?? validateTypeName(toPascalCase(singularizeTableName(tableDetail.name)));
559
+ const foreignKeyByColumn = new Map();
560
+
561
+ for (const fk of tableDetail.foreignKeys ?? []) {
562
+ for (const mapping of fk.mappings ?? []) {
563
+ foreignKeyByColumn.set(mapping.from, {
564
+ table: fk.referencedTable,
565
+ column: mapping.to ?? null,
566
+ });
567
+ }
568
+ }
569
+
570
+ const columns = (tableDetail.columns ?? [])
571
+ .filter((column) => options.includeGeneratedColumns || !column.generated)
572
+ .filter((column) => options.includeHiddenColumns || column.visible !== false || column.generated)
573
+ .map((column) => {
574
+ const matches = checkAnalysis.matchesByColumn.get(column.name) ?? [];
575
+ const normalizedType = normalizeColumnType(column, matches);
576
+ let allowedValues = null;
577
+ if (matches.length === 1 && matches[0].supported && normalizedType !== "boolean") {
578
+ allowedValues = matches[0].values;
579
+ } else if (matches.length > 1) {
580
+ warnings.push(`Multiple CHECK constraints affect column "${column.name}". The allowed value set could not be determined safely.`);
581
+ }
582
+ if (String(column.declaredType ?? "").trim() === "" || normalizedType === "unknown") {
583
+ warnings.push(`Column "${column.name}" uses an unknown or empty declared SQLite type and was mapped to unknown.`);
584
+ }
585
+ return {
586
+ databaseName: column.name,
587
+ propertyName: transformPropertyName(column.name, options.propertyNaming),
588
+ declaredType: column.declaredType || null,
589
+ normalizedType,
590
+ nullable: isColumnNullable(column, tableDetail),
591
+ primaryKey: Number(column.primaryKeyPosition ?? 0) > 0,
592
+ primaryKeyPosition: Number(column.primaryKeyPosition ?? 0) || null,
593
+ defaultValue: column.defaultValue ?? null,
594
+ generated: Boolean(column.generated),
595
+ hidden: column.visible === false,
596
+ foreignKey: foreignKeyByColumn.get(column.name) ?? null,
597
+ allowedValues,
598
+ checkConstraints: matches.map((match) => match.expression),
599
+ warnings: [],
600
+ };
601
+ });
602
+
603
+ if (!columns.length) {
604
+ warnings.push(`Table "${tableDetail.name}" has no columns available for the selected options.`);
605
+ }
606
+
607
+ const metadata = {
608
+ columnCount: columns.length,
609
+ generatedColumnCount: columns.filter((column) => column.generated).length,
610
+ hiddenColumnCount: columns.filter((column) => column.hidden).length,
611
+ checkConstraintsFound: checkAnalysis.expressions.length,
612
+ checkConstraintsApplied: columns.filter((column) => column.allowedValues?.length || column.normalizedType === "boolean").length,
613
+ checkConstraintsIgnored: Math.max(0, checkAnalysis.expressions.length - columns.filter((column) => column.allowedValues?.length || column.normalizedType === "boolean").length),
614
+ };
615
+ if (metadata.checkConstraintsIgnored > 0) {
616
+ warnings.push(
617
+ `Some CHECK constraints for table "${tableDetail.name}" could not be evaluated safely and were ignored.`
618
+ );
619
+ }
620
+ const table = {
621
+ tableName: tableDetail.name,
622
+ suggestedTypeName: typeName,
623
+ createTableSql: ddl,
624
+ columns,
625
+ };
626
+ const code =
627
+ target === "typescript" ? generateTypeScript(table, options)
628
+ : target === "rust" ? generateRust(table, options)
629
+ : target === "kotlin" ? generateKotlin(table, options)
630
+ : generateSwift(table, options);
631
+
632
+ if (target === "swift" && columns.some((column) => ["json", "unknown"].includes(column.normalizedType))) {
633
+ warnings.push("The JSONValue or AnyCodable type must be provided by your Swift project.");
634
+ }
635
+
636
+ return makeResult(target, table, options, code, warnings, metadata);
637
+ }
638
+
639
+ assertOutputPath(result, outputPath, { force = false } = {}) {
640
+ const expectedExtension = FILE_EXTENSIONS[result.target];
641
+ if (path.extname(outputPath) !== expectedExtension) {
642
+ throw new ValidationError(`Output file for ${result.target} must use ${expectedExtension}.`, {
643
+ code: "INVALID_TYPE_OPTIONS",
644
+ });
645
+ }
646
+ if (!force && require("node:fs").existsSync(outputPath)) {
647
+ throw new ValidationError(`Output file already exists: ${outputPath}`, {
648
+ code: "INVALID_TYPE_OPTIONS",
649
+ });
650
+ }
651
+ }
652
+ }
653
+
654
+ module.exports = {
655
+ FILE_EXTENSIONS,
656
+ TypeGenerationService,
657
+ normalizeTarget,
658
+ normalizeOptions,
659
+ transformPropertyName,
660
+ toCamelCase,
661
+ toPascalCase,
662
+ toSnakeCase,
663
+ };
@@ -56,7 +56,6 @@ module.exports = {
56
56
  "secondary-fixed": "#e5e2e1",
57
57
  },
58
58
  fontFamily: {
59
- headline: "var(--font-family-headline)",
60
59
  body: "var(--font-family-body)",
61
60
  label: "var(--font-family-body)",
62
61
  mono: "var(--font-family-mono)",
@@ -1,2 +0,0 @@
1
- github: oliverjessner
2
- buy_me_a_coffee: oliverjessner