sqlite-hub 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +15 -10
  2. package/docs/MCP.md +36 -4
  3. package/docs/changelog.md +11 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/settings.js +2 -2
  61. package/frontend/js/views/structure.js +28 -5
  62. package/frontend/js/views/tableAdvisor.js +14 -6
  63. package/frontend/js/views/tableDesigner.js +44 -9
  64. package/frontend/styles/components.css +676 -79
  65. package/frontend/styles/structure-graph.css +49 -0
  66. package/frontend/styles/tailwind.generated.css +1 -1
  67. package/package.json +1 -1
  68. package/server/mcp/httpRouter.js +95 -0
  69. package/server/routes/connections.js +12 -0
  70. package/server/routes/data.js +1 -1
  71. package/server/routes/settings.js +29 -7
  72. package/server/routes/tableDesigner.js +15 -1
  73. package/server/server.js +15 -0
  74. package/server/services/mcpToolService.js +28 -0
  75. package/server/services/sqlite/connectionManager.js +23 -2
  76. package/server/services/sqlite/dataBrowserService.js +21 -0
  77. package/server/services/sqlite/introspection.js +39 -1
  78. package/server/services/sqlite/structureService.js +66 -11
  79. package/server/services/sqlite/tableAdvisor.js +6 -0
  80. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  81. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  82. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  83. package/server/services/sqlite/tableDesignerService.js +110 -5
  84. package/server/services/storage/appStateStore.js +303 -4
  85. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  86. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  87. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  88. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  89. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -1,4 +1,5 @@
1
1
  const { mapSqliteError, ValidationError } = require("../../utils/errors");
2
+ const { quoteIdentifier } = require("../../utils/identifier");
2
3
  const { getTableDetail } = require("./introspection");
3
4
  const { analyzeTableDesignerChanges } = require("./tableDesigner/changeAnalysis");
4
5
  const {
@@ -19,15 +20,31 @@ function getImportedFillColumns(draft) {
19
20
  );
20
21
  }
21
22
 
23
+ function getImportedRows(draft) {
24
+ return Array.isArray(draft.importRows) && draft.importRows.length
25
+ ? draft.importRows
26
+ : draft.importedCsvRows ?? [];
27
+ }
28
+
22
29
  function coerceImportedCellValue(column, value) {
23
- const textValue = String(value ?? "");
24
- const trimmedValue = textValue.trim();
30
+ if (value === null || value === undefined) {
31
+ return null;
32
+ }
33
+
34
+ const normalizedValue =
35
+ typeof value === "object" ? JSON.stringify(value) : value;
25
36
  const type = normalizeDesignerType(column.type);
37
+ const textValue = String(normalizedValue ?? "");
38
+ const trimmedValue = textValue.trim();
26
39
 
27
40
  if (!trimmedValue) {
28
41
  return ["TEXT", "DATE", "DATETIME"].includes(type) ? "" : null;
29
42
  }
30
43
 
44
+ if (typeof normalizedValue === "boolean") {
45
+ return normalizedValue ? 1 : 0;
46
+ }
47
+
31
48
  if (type === "BOOLEAN") {
32
49
  const normalized = trimmedValue.toLowerCase();
33
50
 
@@ -54,6 +71,35 @@ function coerceImportedCellValue(column, value) {
54
71
  return textValue;
55
72
  }
56
73
 
74
+ function normalizeCheckExpressionSql(expression) {
75
+ const normalized = String(expression ?? "").trim();
76
+
77
+ if (/^CHECK\s*\(/i.test(normalized)) {
78
+ return normalized;
79
+ }
80
+
81
+ return `CHECK (${normalized})`;
82
+ }
83
+
84
+ function assertSafeCheckExpressionFragment(expression) {
85
+ const normalized = String(expression ?? "").trim();
86
+
87
+ if (
88
+ normalized.includes("\0") ||
89
+ normalized.includes(";") ||
90
+ normalized.includes("--") ||
91
+ normalized.includes("/*") ||
92
+ normalized.includes("*/")
93
+ ) {
94
+ throw new ValidationError(
95
+ "CHECK expression must be a single SQL fragment without statement separators or comments.",
96
+ {
97
+ code: "CHECK_EXPRESSION_INVALID_FRAGMENT",
98
+ }
99
+ );
100
+ }
101
+ }
102
+
57
103
  class TableDesignerService {
58
104
  constructor({ connectionManager }) {
59
105
  this.connectionManager = connectionManager;
@@ -126,7 +172,9 @@ class TableDesignerService {
126
172
  const executeCreateWithImport = db.transaction((nextDraft) => {
127
173
  db.exec(buildCreateTableSql(nextDraft));
128
174
 
129
- if (!nextDraft.fillImportedRows || !nextDraft.importedCsvRows.length) {
175
+ const importRows = getImportedRows(nextDraft);
176
+
177
+ if (!nextDraft.fillImportedRows || !importRows.length) {
130
178
  return;
131
179
  }
132
180
 
@@ -143,10 +191,10 @@ class TableDesignerService {
143
191
  )
144
192
  );
145
193
 
146
- nextDraft.importedCsvRows.forEach((row) => {
194
+ importRows.forEach((row) => {
147
195
  insertStatement.run(
148
196
  importedColumns.map((column) =>
149
- coerceImportedCellValue(column, row[column.importedValueIndex] ?? "")
197
+ coerceImportedCellValue(column, row[column.importedValueIndex])
150
198
  )
151
199
  );
152
200
  });
@@ -174,6 +222,63 @@ class TableDesignerService {
174
222
  tables: listDesignerTables(db),
175
223
  };
176
224
  }
225
+
226
+ validateCheckExpression(payload = {}) {
227
+ const db = this.connectionManager.getActiveDatabase();
228
+ const draft = normalizeDraftPayload(payload.draft ?? {});
229
+ const expression = String(payload.expression ?? "").trim();
230
+
231
+ if (!expression) {
232
+ throw new ValidationError("CHECK expression is required.", {
233
+ code: "CHECK_EXPRESSION_REQUIRED",
234
+ });
235
+ }
236
+
237
+ assertSafeCheckExpressionFragment(expression);
238
+
239
+ const columns = draft.columns.filter((column) => !column.deleted && column.name);
240
+
241
+ if (!columns.length) {
242
+ throw new ValidationError("At least one named column is required to validate a CHECK expression.", {
243
+ code: "CHECK_VALIDATION_COLUMNS_REQUIRED",
244
+ });
245
+ }
246
+
247
+ const tempTableName = `__sqlite_hub_check_validation_${Date.now()}_${Math.random()
248
+ .toString(16)
249
+ .slice(2)}`;
250
+ const tempDraft = {
251
+ ...draft,
252
+ tableName: tempTableName,
253
+ columns,
254
+ checkConstraints: [
255
+ {
256
+ id: "check:validation",
257
+ expression,
258
+ },
259
+ ],
260
+ };
261
+ const createSql = buildCreateTableSql(tempDraft).replace(/^CREATE TABLE\s+/i, "CREATE TEMP TABLE ");
262
+ const dropSql = `DROP TABLE IF EXISTS ${quoteIdentifier(tempTableName)};`;
263
+
264
+ try {
265
+ db.exec(dropSql);
266
+ db.exec(createSql);
267
+ db.exec(dropSql);
268
+ return {
269
+ valid: true,
270
+ generatedSql: normalizeCheckExpressionSql(expression),
271
+ };
272
+ } catch (error) {
273
+ try {
274
+ db.exec(dropSql);
275
+ } catch {
276
+ // Ignore cleanup failures; the validation error below is the useful signal.
277
+ }
278
+
279
+ throw mapSqliteError(error);
280
+ }
281
+ }
177
282
  }
178
283
 
179
284
  module.exports = {
@@ -40,6 +40,7 @@ const CONNECTION_LOGO_DIRECTORY = "db_logos";
40
40
  const LEGACY_STATE_FILENAME = "app-state.json";
41
41
  const STATE_DATABASE_FILENAME = "sqlite-hub-state.db";
42
42
  const MAX_CONNECTION_LOGO_SIZE_BYTES = 5 * 1024 * 1024;
43
+ const MAX_CONNECTION_TAG_NAME_LENGTH = 40;
43
44
  const MAX_DOCUMENT_CONTENT_BYTES = 5 * 1024 * 1024;
44
45
  const MAX_DOCUMENT_FILENAME_LENGTH = 160;
45
46
  const QUERY_EXECUTION_SOURCES = new Set(["api", "cli", "user", "mcp"]);
@@ -251,6 +252,44 @@ const CONNECTION_LOGO_EXTENSION_BY_FILE_EXTENSION = {
251
252
  ".png": "png",
252
253
  ".webp": "webp",
253
254
  };
255
+
256
+ function normalizeConnectionTagName(value) {
257
+ const normalizedName = String(value ?? "")
258
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
259
+ .trim();
260
+
261
+ if (!normalizedName) {
262
+ throw new ValidationError("Tag name is required.");
263
+ }
264
+
265
+ if (normalizedName.length > MAX_CONNECTION_TAG_NAME_LENGTH) {
266
+ throw new ValidationError(
267
+ `Tag name must be ${MAX_CONNECTION_TAG_NAME_LENGTH} characters or fewer.`
268
+ );
269
+ }
270
+
271
+ return normalizedName;
272
+ }
273
+
274
+ function normalizeConnectionTagNames(values = []) {
275
+ if (!Array.isArray(values)) {
276
+ return [];
277
+ }
278
+
279
+ const namesByKey = new Map();
280
+
281
+ for (const value of values) {
282
+ const name = normalizeConnectionTagName(value);
283
+ const key = name.toLowerCase();
284
+
285
+ if (!namesByKey.has(key)) {
286
+ namesByKey.set(key, name);
287
+ }
288
+ }
289
+
290
+ return [...namesByKey.values()];
291
+ }
292
+
254
293
  const MEDIA_TAGGING_CONFIG_FIELDS = [
255
294
  {
256
295
  column: "tag_table",
@@ -464,6 +503,29 @@ class AppStateStore {
464
503
  CREATE INDEX IF NOT EXISTS idx_recent_connections_last_opened
465
504
  ON recent_connections(lastOpenedAt DESC, id ASC);
466
505
 
506
+ CREATE TABLE IF NOT EXISTS tags (
507
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
508
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE,
509
+ createdAt TEXT NOT NULL,
510
+ updatedAt TEXT NOT NULL
511
+ );
512
+
513
+ CREATE TABLE IF NOT EXISTS connection_tags (
514
+ connectionId TEXT NOT NULL,
515
+ tagId INTEGER NOT NULL,
516
+ PRIMARY KEY (connectionId, tagId),
517
+ FOREIGN KEY (connectionId)
518
+ REFERENCES recent_connections(id)
519
+ ON UPDATE CASCADE
520
+ ON DELETE CASCADE,
521
+ FOREIGN KEY (tagId)
522
+ REFERENCES tags(id)
523
+ ON DELETE CASCADE
524
+ );
525
+
526
+ CREATE INDEX IF NOT EXISTS idx_connection_tags_tag
527
+ ON connection_tags(tagId, connectionId);
528
+
467
529
  CREATE TABLE IF NOT EXISTS query_history (
468
530
  id INTEGER PRIMARY KEY AUTOINCREMENT,
469
531
  database_key TEXT NOT NULL,
@@ -2917,8 +2979,142 @@ class AppStateStore {
2917
2979
  });
2918
2980
  }
2919
2981
 
2920
- getRecentConnections() {
2982
+ decorateConnectionTag(row = {}) {
2983
+ return {
2984
+ id: Number(row.id),
2985
+ name: String(row.name ?? ""),
2986
+ createdAt: row.createdAt ?? null,
2987
+ updatedAt: row.updatedAt ?? null,
2988
+ ...(row.connectionCount === undefined
2989
+ ? {}
2990
+ : { connectionCount: Number(row.connectionCount ?? 0) }),
2991
+ };
2992
+ }
2993
+
2994
+ getConnectionTagsByConnectionIds(connectionIds = []) {
2995
+ const ids = connectionIds
2996
+ .map((id) => String(id ?? "").trim())
2997
+ .filter(Boolean)
2998
+ .filter((id, index, values) => values.indexOf(id) === index);
2999
+
3000
+ if (!ids.length) {
3001
+ return new Map();
3002
+ }
3003
+
3004
+ const placeholders = ids.map(() => "?").join(", ");
3005
+ const rows = this.db
3006
+ .prepare(
3007
+ `
3008
+ SELECT
3009
+ ct.connectionId,
3010
+ t.id,
3011
+ t.name,
3012
+ t.createdAt,
3013
+ t.updatedAt
3014
+ FROM connection_tags ct
3015
+ INNER JOIN tags t
3016
+ ON t.id = ct.tagId
3017
+ WHERE ct.connectionId IN (${placeholders})
3018
+ ORDER BY t.name COLLATE NOCASE ASC, t.id ASC
3019
+ `
3020
+ )
3021
+ .all(...ids);
3022
+ const tagsByConnectionId = new Map(ids.map((id) => [id, []]));
3023
+
3024
+ for (const row of rows) {
3025
+ tagsByConnectionId
3026
+ .get(row.connectionId)
3027
+ ?.push(this.decorateConnectionTag(row));
3028
+ }
3029
+
3030
+ return tagsByConnectionId;
3031
+ }
3032
+
3033
+ getConnectionTags(connectionId) {
3034
+ const normalizedConnectionId = String(connectionId ?? "").trim();
3035
+
3036
+ if (!normalizedConnectionId) {
3037
+ return [];
3038
+ }
3039
+
3040
+ return this.getConnectionTagsByConnectionIds([normalizedConnectionId]).get(
3041
+ normalizedConnectionId
3042
+ ) ?? [];
3043
+ }
3044
+
3045
+ listConnectionTags() {
2921
3046
  return this.db
3047
+ .prepare(
3048
+ `
3049
+ SELECT
3050
+ t.id,
3051
+ t.name,
3052
+ t.createdAt,
3053
+ t.updatedAt,
3054
+ COUNT(ct.connectionId) AS connectionCount
3055
+ FROM tags t
3056
+ LEFT JOIN connection_tags ct
3057
+ ON ct.tagId = t.id
3058
+ GROUP BY t.id
3059
+ ORDER BY t.name COLLATE NOCASE ASC, t.id ASC
3060
+ `
3061
+ )
3062
+ .all()
3063
+ .map((row) => this.decorateConnectionTag(row));
3064
+ }
3065
+
3066
+ getConnectionTagById(tagId) {
3067
+ const row = this.db
3068
+ .prepare(
3069
+ `
3070
+ SELECT id, name, createdAt, updatedAt
3071
+ FROM tags
3072
+ WHERE id = ?
3073
+ `
3074
+ )
3075
+ .get(Number(tagId));
3076
+
3077
+ return row ? this.decorateConnectionTag(row) : null;
3078
+ }
3079
+
3080
+ getConnectionTagByName(name) {
3081
+ const normalizedName = normalizeConnectionTagName(name);
3082
+ const row = this.db
3083
+ .prepare(
3084
+ `
3085
+ SELECT id, name, createdAt, updatedAt
3086
+ FROM tags
3087
+ WHERE name = ? COLLATE NOCASE
3088
+ `
3089
+ )
3090
+ .get(normalizedName);
3091
+
3092
+ return row ? this.decorateConnectionTag(row) : null;
3093
+ }
3094
+
3095
+ getOrCreateConnectionTag(name) {
3096
+ const normalizedName = normalizeConnectionTagName(name);
3097
+ const existingTag = this.getConnectionTagByName(normalizedName);
3098
+
3099
+ if (existingTag) {
3100
+ return existingTag;
3101
+ }
3102
+
3103
+ const timestamp = new Date().toISOString();
3104
+ const result = this.db
3105
+ .prepare(
3106
+ `
3107
+ INSERT INTO tags (name, createdAt, updatedAt)
3108
+ VALUES (?, ?, ?)
3109
+ `
3110
+ )
3111
+ .run(normalizedName, timestamp, timestamp);
3112
+
3113
+ return this.getConnectionTagById(result.lastInsertRowid);
3114
+ }
3115
+
3116
+ getRecentConnections() {
3117
+ const rows = this.db
2922
3118
  .prepare(`
2923
3119
  SELECT
2924
3120
  id,
@@ -2932,8 +3128,108 @@ class AppStateStore {
2932
3128
  FROM recent_connections
2933
3129
  ORDER BY lastOpenedAt DESC, id ASC
2934
3130
  `)
2935
- .all()
2936
- .map((connection) => this.decorateConnection(connection));
3131
+ .all();
3132
+ const tagsByConnectionId = this.getConnectionTagsByConnectionIds(
3133
+ rows.map((connection) => connection.id)
3134
+ );
3135
+
3136
+ return rows.map((connection) =>
3137
+ this.decorateConnection(
3138
+ connection,
3139
+ tagsByConnectionId.get(connection.id) ?? []
3140
+ )
3141
+ );
3142
+ }
3143
+
3144
+ getRecentConnection(id) {
3145
+ return (
3146
+ this.getRecentConnections().find(
3147
+ (connection) => connection.id === String(id ?? "").trim()
3148
+ ) ?? null
3149
+ );
3150
+ }
3151
+
3152
+ assertRecentConnectionExists(id) {
3153
+ const normalizedConnectionId = String(id ?? "").trim();
3154
+
3155
+ if (!normalizedConnectionId) {
3156
+ throw new ValidationError("Connection id is required.");
3157
+ }
3158
+
3159
+ const row = this.db
3160
+ .prepare("SELECT id FROM recent_connections WHERE id = ?")
3161
+ .get(normalizedConnectionId);
3162
+
3163
+ if (!row) {
3164
+ throw new NotFoundError(`Recent connection not found: ${normalizedConnectionId}`);
3165
+ }
3166
+
3167
+ return normalizedConnectionId;
3168
+ }
3169
+
3170
+ setConnectionTags(connectionId, tagNames = []) {
3171
+ const normalizedConnectionId = this.assertRecentConnectionExists(connectionId);
3172
+ const normalizedTagNames = normalizeConnectionTagNames(tagNames);
3173
+
3174
+ this.db.transaction(() => {
3175
+ this.db
3176
+ .prepare("DELETE FROM connection_tags WHERE connectionId = ?")
3177
+ .run(normalizedConnectionId);
3178
+
3179
+ for (const tagName of normalizedTagNames) {
3180
+ const tag = this.getOrCreateConnectionTag(tagName);
3181
+
3182
+ this.db
3183
+ .prepare(
3184
+ `
3185
+ INSERT OR IGNORE INTO connection_tags (connectionId, tagId)
3186
+ VALUES (?, ?)
3187
+ `
3188
+ )
3189
+ .run(normalizedConnectionId, tag.id);
3190
+ }
3191
+ })();
3192
+
3193
+ return this.getConnectionTags(normalizedConnectionId);
3194
+ }
3195
+
3196
+ addConnectionTag(connectionId, tagName) {
3197
+ const normalizedConnectionId = this.assertRecentConnectionExists(connectionId);
3198
+ const tag = this.getOrCreateConnectionTag(tagName);
3199
+
3200
+ this.db
3201
+ .prepare(
3202
+ `
3203
+ INSERT OR IGNORE INTO connection_tags (connectionId, tagId)
3204
+ VALUES (?, ?)
3205
+ `
3206
+ )
3207
+ .run(normalizedConnectionId, tag.id);
3208
+
3209
+ return this.getConnectionTags(normalizedConnectionId);
3210
+ }
3211
+
3212
+ removeConnectionTag(connectionId, tagId) {
3213
+ const normalizedConnectionId = this.assertRecentConnectionExists(connectionId);
3214
+
3215
+ this.db
3216
+ .prepare(
3217
+ `
3218
+ DELETE FROM connection_tags
3219
+ WHERE connectionId = ? AND tagId = ?
3220
+ `
3221
+ )
3222
+ .run(normalizedConnectionId, Number(tagId));
3223
+
3224
+ return this.getConnectionTags(normalizedConnectionId);
3225
+ }
3226
+
3227
+ deleteConnectionTag(tagId) {
3228
+ const result = this.db
3229
+ .prepare("DELETE FROM tags WHERE id = ?")
3230
+ .run(Number(tagId));
3231
+
3232
+ return result.changes > 0;
2937
3233
  }
2938
3234
 
2939
3235
  upsertRecentConnection(connection, options = {}) {
@@ -3121,7 +3417,7 @@ class AppStateStore {
3121
3417
  );
3122
3418
  }
3123
3419
 
3124
- decorateConnection(connection = {}) {
3420
+ decorateConnection(connection = {}, tags = []) {
3125
3421
  const logoPath = this.normalizeLogoPath(connection.logoPath);
3126
3422
 
3127
3423
  return {
@@ -3129,6 +3425,7 @@ class AppStateStore {
3129
3425
  readOnly: Boolean(connection.readOnly),
3130
3426
  logoPath,
3131
3427
  logoUrl: this.getConnectionLogoUrl(logoPath),
3428
+ tags: Array.isArray(tags) ? tags : [],
3132
3429
  };
3133
3430
  }
3134
3431
 
@@ -3696,4 +3993,6 @@ class AppStateStore {
3696
3993
  module.exports = {
3697
3994
  AppStateStore,
3698
3995
  DEFAULT_STATE,
3996
+ MAX_CONNECTION_TAG_NAME_LENGTH,
3997
+ normalizeConnectionTagName,
3699
3998
  };