sqlite-hub 2.1.0 → 2.2.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.
@@ -10,6 +10,7 @@ const {
10
10
  } = require("../services/appInfoService");
11
11
  const { McpStatusService } = require("../services/mcpStatusService");
12
12
  const { MCP_TOOL_DEFINITIONS } = require("../services/mcpToolService");
13
+ const { recordUserAction } = require("../utils/userActionLog");
13
14
 
14
15
  function getActiveTokenContext({ connectionManager, tokenService }) {
15
16
  const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
@@ -161,6 +162,20 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
161
162
  const activeDatabase = requireActiveDatabase(connectionManager);
162
163
  const token = tokenService.createToken(activeDatabase.id, req.body?.name);
163
164
 
165
+ recordUserAction({
166
+ appStateStore,
167
+ connectionManager,
168
+ action: "settings.api-token.create",
169
+ targetType: "api-token",
170
+ targetName: token.name ?? token.id,
171
+ databaseKey: activeDatabase.id,
172
+ metadata: {
173
+ apiTokenId: token.id,
174
+ apiTokenName: token.name ?? null,
175
+ tokenPrefix: token.tokenPrefix ?? null,
176
+ },
177
+ });
178
+
164
179
  res.status(201).json(
165
180
  successResponse({
166
181
  message: "API token created. It will only be shown once.",
@@ -175,8 +190,25 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
175
190
  "/api-tokens/:tokenId",
176
191
  route((req, res) => {
177
192
  const activeDatabase = requireActiveDatabase(connectionManager);
193
+ const token = tokenService
194
+ .listTokens(activeDatabase.id)
195
+ .find((candidate) => candidate.id === req.params.tokenId);
178
196
  const result = tokenService.deleteToken(activeDatabase.id, req.params.tokenId);
179
197
 
198
+ recordUserAction({
199
+ appStateStore,
200
+ connectionManager,
201
+ action: "settings.api-token.delete",
202
+ targetType: "api-token",
203
+ targetName: token?.name ?? req.params.tokenId,
204
+ databaseKey: activeDatabase.id,
205
+ metadata: {
206
+ apiTokenId: req.params.tokenId,
207
+ apiTokenName: token?.name ?? null,
208
+ tokenPrefix: token?.tokenPrefix ?? null,
209
+ },
210
+ });
211
+
180
212
  res.json(
181
213
  successResponse({
182
214
  message: "API token deleted.",
@@ -1,7 +1,8 @@
1
1
  const express = require("express");
2
2
  const { route, successResponse } = require("../utils/errors");
3
+ const { recordUserAction } = require("../utils/userActionLog");
3
4
 
4
- function createTableDesignerRouter({ tableDesignerService }) {
5
+ function createTableDesignerRouter({ tableDesignerService, appStateStore = null, connectionManager = null }) {
5
6
  const router = express.Router();
6
7
 
7
8
  router.get(
@@ -52,6 +53,24 @@ function createTableDesignerRouter({ tableDesignerService }) {
52
53
  const data = tableDesignerService.saveDraft(req.body ?? {});
53
54
  const isCreate = String(req.body?.draft?.mode ?? req.body?.mode ?? "").trim() !== "edit";
54
55
  const fillsImportedRows = Boolean(req.body?.draft?.fillImportedRows ?? req.body?.fillImportedRows);
56
+ const importedRows = req.body?.draft?.importRows ?? req.body?.draft?.importedCsvRows ?? [];
57
+ const executedSqlCount = Array.isArray(data.executedSql) ? data.executedSql.length : 0;
58
+
59
+ if (isCreate || executedSqlCount > 0) {
60
+ recordUserAction({
61
+ appStateStore,
62
+ connectionManager,
63
+ action: isCreate ? "table-designer.table.create" : "table-designer.table.update",
64
+ targetType: "table",
65
+ targetName: data.savedTableName ?? req.body?.draft?.tableName ?? req.body?.tableName,
66
+ metadata: {
67
+ mode: isCreate ? "create" : "edit",
68
+ executedSqlCount,
69
+ fillsImportedRows,
70
+ importedRowCount: fillsImportedRows && Array.isArray(importedRows) ? importedRows.length : 0,
71
+ },
72
+ });
73
+ }
55
74
 
56
75
  res.json(
57
76
  successResponse({
package/server/server.js CHANGED
@@ -129,13 +129,13 @@ app.use(
129
129
  nativeFileDialogService,
130
130
  })
131
131
  );
132
- app.use("/api/backups", createBackupsRouter({ backupService }));
132
+ app.use("/api/backups", createBackupsRouter({ backupService, appStateStore, connectionManager }));
133
133
  app.use("/api/db", createOverviewRouter({ overviewService }));
134
134
  app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
135
135
  app.use("/api/charts", createChartsRouter({ appStateStore, connectionManager, sqlExecutor }));
136
136
  app.use("/api/structure", createStructureRouter({ structureService }));
137
- app.use("/api/data", createDataRouter({ dataBrowserService }));
138
- app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
137
+ app.use("/api/data", createDataRouter({ dataBrowserService, appStateStore, connectionManager }));
138
+ app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService, appStateStore, connectionManager }));
139
139
  app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
140
140
  app.use(
141
141
  "/api/settings",
@@ -77,7 +77,7 @@ const MCP_TOOL_DEFINITIONS = [
77
77
  ),
78
78
  },
79
79
  {
80
- name: "get_stored_queries",
80
+ name: "get_saved_queries",
81
81
  description: "List saved SQL Editor queries for a database. This is the MCP equivalent of `sqlite-hub --database:name --queries`.",
82
82
  inputSchema: objectSchema({
83
83
  databaseId: databaseIdProperty(),
@@ -164,6 +164,7 @@ function redactConnection(connection = {}) {
164
164
  label: connection.label,
165
165
  readOnly: Boolean(connection.readOnly),
166
166
  sizeBytes: connection.sizeBytes ?? null,
167
+ createdAt: connection.createdAt ?? null,
167
168
  lastOpenedAt: connection.lastOpenedAt ?? null,
168
169
  lastModifiedAt: connection.lastModifiedAt ?? null,
169
170
  };
@@ -176,6 +177,7 @@ function redactOverview(overview = {}) {
176
177
  file: {
177
178
  filename: overview.file?.filename ?? overview.connection?.label ?? null,
178
179
  sizeBytes: overview.file?.sizeBytes ?? null,
180
+ createdAt: overview.file?.createdAt ?? null,
179
181
  lastModifiedAt: overview.file?.lastModifiedAt ?? null,
180
182
  },
181
183
  };
@@ -249,7 +251,7 @@ class McpToolService {
249
251
  executedBy: "mcp",
250
252
  maxRows: args.maxRows,
251
253
  });
252
- case "get_stored_queries":
254
+ case "get_saved_queries":
253
255
  return this.databaseService.listSavedQueries(args.databaseId, args.limit);
254
256
  case "execute_stored_query":
255
257
  return this.databaseService.executeSavedQuery(args.databaseId, args.queryName, {
@@ -70,6 +70,7 @@ class ConnectionManager {
70
70
  label: options.label?.trim() || path.basename(filePath),
71
71
  path: filePath,
72
72
  lastOpenedAt: new Date().toISOString(),
73
+ createdAt: metadata.createdAt,
73
74
  lastModifiedAt: metadata.lastModifiedAt,
74
75
  sizeBytes: metadata.sizeBytes,
75
76
  readOnly: options.readOnly ?? !isWritable(filePath),
@@ -276,6 +277,7 @@ class ConnectionManager {
276
277
  ...existing,
277
278
  label: normalizedLabel,
278
279
  path: resolvedPath,
280
+ createdAt: metadata.createdAt,
279
281
  lastModifiedAt: metadata.lastModifiedAt,
280
282
  sizeBytes: metadata.sizeBytes,
281
283
  readOnly: normalizedReadOnly,
@@ -123,6 +123,7 @@ class OverviewService {
123
123
  filename: connection.label,
124
124
  path: connection.path,
125
125
  sizeBytes: connection.sizeBytes,
126
+ createdAt: connection.createdAt,
126
127
  lastModifiedAt: connection.lastModifiedAt,
127
128
  },
128
129
  sqlite: {
@@ -44,7 +44,7 @@ const MAX_CONNECTION_TAG_NAME_LENGTH = 40;
44
44
  const MAX_DOCUMENT_CONTENT_BYTES = 5 * 1024 * 1024;
45
45
  const MAX_DOCUMENT_FILENAME_LENGTH = 160;
46
46
  const QUERY_EXECUTION_SOURCES = new Set(["api", "cli", "user", "mcp"]);
47
- const ACCESS_LOG_SOURCES = new Set(["api", "cli"]);
47
+ const ACCESS_LOG_SOURCES = new Set(["api", "cli", "user"]);
48
48
  const ACCESS_LOG_STATUSES = new Set(["success", "error"]);
49
49
  const MAX_ACCESS_LOG_TEXT_LENGTH = 500;
50
50
  const MAX_ACCESS_LOG_METADATA_BYTES = 16 * 1024;
@@ -78,6 +78,32 @@ function normalizeDocumentId(documentId) {
78
78
  return normalizedDocumentId;
79
79
  }
80
80
 
81
+ function normalizeDocumentFolderId(folderId) {
82
+ const normalizedFolderId = String(folderId ?? "").trim();
83
+
84
+ return normalizedFolderId || null;
85
+ }
86
+
87
+ function normalizeDocumentFolderName(name) {
88
+ const normalizedName = String(name ?? "")
89
+ .trim()
90
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
91
+ .replace(/[\\/]+/g, " ")
92
+ .replace(/\s+/g, " ")
93
+ .replace(/^\.+/, "")
94
+ .trim();
95
+
96
+ if (!normalizedName) {
97
+ throw new ValidationError("Folder name is required.");
98
+ }
99
+
100
+ if (normalizedName.length > 80) {
101
+ throw new ValidationError("Folder name must not exceed 80 characters.");
102
+ }
103
+
104
+ return normalizedName;
105
+ }
106
+
81
107
  function splitMarkdownFilename(filename) {
82
108
  const normalizedFilename = String(filename ?? "");
83
109
  const extensionMatch = normalizedFilename.match(/\.md$/i);
@@ -621,6 +647,7 @@ class AppStateStore {
621
647
  CREATE TABLE IF NOT EXISTS database_documents (
622
648
  id TEXT PRIMARY KEY,
623
649
  database_key TEXT NOT NULL,
650
+ folder_id TEXT,
624
651
  title TEXT NOT NULL,
625
652
  filename TEXT NOT NULL,
626
653
  content TEXT NOT NULL DEFAULT '',
@@ -632,6 +659,18 @@ class AppStateStore {
632
659
  CREATE INDEX IF NOT EXISTS idx_database_documents_database_updated
633
660
  ON database_documents(database_key, updated_at DESC, id ASC);
634
661
 
662
+ CREATE TABLE IF NOT EXISTS database_document_folders (
663
+ id TEXT PRIMARY KEY,
664
+ database_key TEXT NOT NULL,
665
+ name TEXT NOT NULL,
666
+ created_at TEXT NOT NULL,
667
+ updated_at TEXT NOT NULL,
668
+ UNIQUE(database_key, name)
669
+ );
670
+
671
+ CREATE INDEX IF NOT EXISTS idx_database_document_folders_database_name
672
+ ON database_document_folders(database_key, name COLLATE NOCASE, id ASC);
673
+
635
674
  CREATE TABLE IF NOT EXISTS api_tokens (
636
675
  id TEXT PRIMARY KEY,
637
676
  database_key TEXT NOT NULL,
@@ -647,7 +686,7 @@ class AppStateStore {
647
686
 
648
687
  CREATE TABLE IF NOT EXISTS access_log (
649
688
  id INTEGER PRIMARY KEY AUTOINCREMENT,
650
- source TEXT NOT NULL CHECK(source IN ('api', 'cli')),
689
+ source TEXT NOT NULL CHECK(source IN ('api', 'cli', 'user')),
651
690
  action TEXT NOT NULL,
652
691
  database_key TEXT,
653
692
  target_type TEXT,
@@ -727,9 +766,113 @@ class AppStateStore {
727
766
  }
728
767
 
729
768
  this.ensureQueryRunsSchema();
769
+ this.ensureAccessLogSchema();
770
+ this.ensureDatabaseDocumentsSchema();
730
771
  this.ensureMediaTaggingConfigSchema();
731
772
  }
732
773
 
774
+ ensureDatabaseDocumentsSchema() {
775
+ const columns = new Set(
776
+ this.db
777
+ .prepare("PRAGMA table_info(database_documents)")
778
+ .all()
779
+ .map((column) => column.name)
780
+ );
781
+
782
+ if (!columns.has("folder_id")) {
783
+ this.db.exec("ALTER TABLE database_documents ADD COLUMN folder_id TEXT");
784
+ }
785
+
786
+ this.db.exec(`
787
+ CREATE INDEX IF NOT EXISTS idx_database_documents_folder_updated
788
+ ON database_documents(database_key, folder_id, updated_at DESC, id ASC);
789
+
790
+ CREATE TABLE IF NOT EXISTS database_document_folders (
791
+ id TEXT PRIMARY KEY,
792
+ database_key TEXT NOT NULL,
793
+ name TEXT NOT NULL,
794
+ created_at TEXT NOT NULL,
795
+ updated_at TEXT NOT NULL,
796
+ UNIQUE(database_key, name)
797
+ );
798
+
799
+ CREATE INDEX IF NOT EXISTS idx_database_document_folders_database_name
800
+ ON database_document_folders(database_key, name COLLATE NOCASE, id ASC);
801
+ `);
802
+ }
803
+
804
+ ensureAccessLogSchema() {
805
+ const row = this.db
806
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'access_log'")
807
+ .get();
808
+ const tableSql = String(row?.sql ?? "");
809
+
810
+ if (tableSql.includes("'user'")) {
811
+ return;
812
+ }
813
+
814
+ this.db.exec(`
815
+ DROP TABLE IF EXISTS access_log_source_migration;
816
+ DROP INDEX IF EXISTS idx_access_log_started;
817
+ DROP INDEX IF EXISTS idx_access_log_source_started;
818
+ DROP INDEX IF EXISTS idx_access_log_database_started;
819
+
820
+ ALTER TABLE access_log RENAME TO access_log_source_migration;
821
+
822
+ CREATE TABLE access_log (
823
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
824
+ source TEXT NOT NULL CHECK(source IN ('api', 'cli', 'user')),
825
+ action TEXT NOT NULL,
826
+ database_key TEXT,
827
+ target_type TEXT,
828
+ target_name TEXT,
829
+ status TEXT NOT NULL CHECK(status IN ('success', 'error')),
830
+ started_at TEXT NOT NULL,
831
+ duration_ms INTEGER,
832
+ error_message TEXT,
833
+ metadata_json TEXT NOT NULL DEFAULT '{}'
834
+ );
835
+
836
+ INSERT INTO access_log (
837
+ id,
838
+ source,
839
+ action,
840
+ database_key,
841
+ target_type,
842
+ target_name,
843
+ status,
844
+ started_at,
845
+ duration_ms,
846
+ error_message,
847
+ metadata_json
848
+ )
849
+ SELECT
850
+ id,
851
+ source,
852
+ action,
853
+ database_key,
854
+ target_type,
855
+ target_name,
856
+ status,
857
+ started_at,
858
+ duration_ms,
859
+ error_message,
860
+ metadata_json
861
+ FROM access_log_source_migration;
862
+
863
+ DROP TABLE access_log_source_migration;
864
+
865
+ CREATE INDEX IF NOT EXISTS idx_access_log_started
866
+ ON access_log(started_at DESC, id DESC);
867
+
868
+ CREATE INDEX IF NOT EXISTS idx_access_log_source_started
869
+ ON access_log(source, started_at DESC, id DESC);
870
+
871
+ CREATE INDEX IF NOT EXISTS idx_access_log_database_started
872
+ ON access_log(database_key, started_at DESC, id DESC);
873
+ `);
874
+ }
875
+
733
876
  ensureQueryRunsSchema() {
734
877
  const queryRunColumns = new Set(
735
878
  this.db
@@ -2244,7 +2387,7 @@ class AppStateStore {
2244
2387
  }
2245
2388
 
2246
2389
  if (actor) {
2247
- if (actor !== "api" && actor !== "cli") {
2390
+ if (actor !== "api" && actor !== "cli" && actor !== "user") {
2248
2391
  return {
2249
2392
  whereSql: "WHERE 1 = 0",
2250
2393
  params: [],
@@ -2409,6 +2552,16 @@ class AppStateStore {
2409
2552
  };
2410
2553
  }
2411
2554
 
2555
+ getLatestActivityLogTimestamp(databaseKey) {
2556
+ const result = this.listActivityLogs({
2557
+ databaseKey,
2558
+ limit: 1,
2559
+ offset: 0,
2560
+ });
2561
+
2562
+ return result.items[0]?.occurredAt ?? null;
2563
+ }
2564
+
2412
2565
  updateQueryHistoryField(historyId, fieldName, value, databaseKey) {
2413
2566
  const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
2414
2567
 
@@ -3692,6 +3845,7 @@ class AppStateStore {
3692
3845
  return {
3693
3846
  id: String(row.id ?? ""),
3694
3847
  databaseKey: row.database_key ?? row.databaseKey ?? "",
3848
+ folderId: row.folder_id ?? row.folderId ?? null,
3695
3849
  title: String(row.title ?? ""),
3696
3850
  filename: String(row.filename ?? ""),
3697
3851
  content: row.content === undefined ? undefined : String(row.content ?? ""),
@@ -3701,6 +3855,115 @@ class AppStateStore {
3701
3855
  };
3702
3856
  }
3703
3857
 
3858
+ decorateDatabaseDocumentFolderRow(row = {}) {
3859
+ return {
3860
+ id: String(row.id ?? ""),
3861
+ databaseKey: row.database_key ?? row.databaseKey ?? "",
3862
+ name: String(row.name ?? ""),
3863
+ createdAt: row.created_at ?? row.createdAt ?? null,
3864
+ updatedAt: row.updated_at ?? row.updatedAt ?? null,
3865
+ };
3866
+ }
3867
+
3868
+ listDatabaseDocumentFolders(databaseKey) {
3869
+ const normalizedDatabaseKey = normalizeDocumentDatabaseKey(databaseKey);
3870
+
3871
+ return this.db
3872
+ .prepare(
3873
+ `
3874
+ SELECT
3875
+ id,
3876
+ database_key,
3877
+ name,
3878
+ created_at,
3879
+ updated_at
3880
+ FROM database_document_folders
3881
+ WHERE database_key = ?
3882
+ ORDER BY name COLLATE NOCASE ASC, id ASC
3883
+ `
3884
+ )
3885
+ .all(normalizedDatabaseKey)
3886
+ .map((row) => this.decorateDatabaseDocumentFolderRow(row));
3887
+ }
3888
+
3889
+ getDatabaseDocumentFolder(databaseKey, folderId) {
3890
+ const normalizedDatabaseKey = normalizeDocumentDatabaseKey(databaseKey);
3891
+ const normalizedFolderId = normalizeDocumentFolderId(folderId);
3892
+
3893
+ if (!normalizedFolderId) {
3894
+ return null;
3895
+ }
3896
+
3897
+ const row = this.db
3898
+ .prepare(
3899
+ `
3900
+ SELECT
3901
+ id,
3902
+ database_key,
3903
+ name,
3904
+ created_at,
3905
+ updated_at
3906
+ FROM database_document_folders
3907
+ WHERE database_key = ?
3908
+ AND id = ?
3909
+ `
3910
+ )
3911
+ .get(normalizedDatabaseKey, normalizedFolderId);
3912
+
3913
+ if (!row) {
3914
+ throw new NotFoundError("Document folder was not found.");
3915
+ }
3916
+
3917
+ return this.decorateDatabaseDocumentFolderRow(row);
3918
+ }
3919
+
3920
+ createDatabaseDocumentFolder(databaseKey, folder = {}) {
3921
+ const normalizedDatabaseKey = normalizeDocumentDatabaseKey(databaseKey);
3922
+ const name = normalizeDocumentFolderName(folder.name);
3923
+ const now = new Date().toISOString();
3924
+ const id = crypto.randomUUID();
3925
+ const existingFolder = this.db
3926
+ .prepare(
3927
+ `
3928
+ SELECT id
3929
+ FROM database_document_folders
3930
+ WHERE database_key = ?
3931
+ AND name = ? COLLATE NOCASE
3932
+ LIMIT 1
3933
+ `
3934
+ )
3935
+ .get(normalizedDatabaseKey, name);
3936
+
3937
+ if (existingFolder) {
3938
+ throw new ConflictError("A folder with this name already exists.");
3939
+ }
3940
+
3941
+ try {
3942
+ this.db
3943
+ .prepare(
3944
+ `
3945
+ INSERT INTO database_document_folders (
3946
+ id,
3947
+ database_key,
3948
+ name,
3949
+ created_at,
3950
+ updated_at
3951
+ )
3952
+ VALUES (?, ?, ?, ?, ?)
3953
+ `
3954
+ )
3955
+ .run(id, normalizedDatabaseKey, name, now, now);
3956
+ } catch (error) {
3957
+ if (error?.code === "SQLITE_CONSTRAINT_UNIQUE") {
3958
+ throw new ConflictError("A folder with this name already exists.");
3959
+ }
3960
+
3961
+ throw error;
3962
+ }
3963
+
3964
+ return this.getDatabaseDocumentFolder(normalizedDatabaseKey, id);
3965
+ }
3966
+
3704
3967
  documentFilenameExists(databaseKey, filename, ignoredDocumentId = null) {
3705
3968
  const row = this.db
3706
3969
  .prepare(
@@ -3749,6 +4012,7 @@ class AppStateStore {
3749
4012
  SELECT
3750
4013
  id,
3751
4014
  database_key,
4015
+ folder_id,
3752
4016
  title,
3753
4017
  filename,
3754
4018
  LENGTH(content) AS content_length,
@@ -3772,6 +4036,7 @@ class AppStateStore {
3772
4036
  SELECT
3773
4037
  id,
3774
4038
  database_key,
4039
+ folder_id,
3775
4040
  title,
3776
4041
  filename,
3777
4042
  content,
@@ -3798,6 +4063,11 @@ class AppStateStore {
3798
4063
  normalizedDatabaseKey,
3799
4064
  normalizeDocumentFilename(document.filename)
3800
4065
  );
4066
+ const folderId = normalizeDocumentFolderId(document.folderId);
4067
+ if (folderId) {
4068
+ this.getDatabaseDocumentFolder(normalizedDatabaseKey, folderId);
4069
+ }
4070
+
3801
4071
  const title = normalizeDocumentTitle(document.title, filename);
3802
4072
  const content = normalizeDocumentContent(document.content);
3803
4073
  const now = new Date().toISOString();
@@ -3809,16 +4079,17 @@ class AppStateStore {
3809
4079
  INSERT INTO database_documents (
3810
4080
  id,
3811
4081
  database_key,
4082
+ folder_id,
3812
4083
  title,
3813
4084
  filename,
3814
4085
  content,
3815
4086
  created_at,
3816
4087
  updated_at
3817
4088
  )
3818
- VALUES (?, ?, ?, ?, ?, ?, ?)
4089
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3819
4090
  `
3820
4091
  )
3821
- .run(id, normalizedDatabaseKey, title, filename, content, now, now);
4092
+ .run(id, normalizedDatabaseKey, folderId, title, filename, content, now, now);
3822
4093
 
3823
4094
  return this.getDatabaseDocument(normalizedDatabaseKey, id);
3824
4095
  }
@@ -3829,6 +4100,15 @@ class AppStateStore {
3829
4100
  const hasFilename = Object.prototype.hasOwnProperty.call(patch, "filename");
3830
4101
  const hasTitle = Object.prototype.hasOwnProperty.call(patch, "title");
3831
4102
  const hasContent = Object.prototype.hasOwnProperty.call(patch, "content");
4103
+ const hasFolderId = Object.prototype.hasOwnProperty.call(patch, "folderId");
4104
+ const folderId = hasFolderId
4105
+ ? normalizeDocumentFolderId(patch.folderId)
4106
+ : existingDocument.folderId;
4107
+
4108
+ if (folderId) {
4109
+ this.getDatabaseDocumentFolder(normalizedDatabaseKey, folderId);
4110
+ }
4111
+
3832
4112
  const filename = hasFilename
3833
4113
  ? this.resolveUniqueDocumentFilename(
3834
4114
  normalizedDatabaseKey,
@@ -3851,6 +4131,7 @@ class AppStateStore {
3851
4131
  `
3852
4132
  UPDATE database_documents
3853
4133
  SET
4134
+ folder_id = ?,
3854
4135
  title = ?,
3855
4136
  filename = ?,
3856
4137
  content = ?,
@@ -3859,7 +4140,7 @@ class AppStateStore {
3859
4140
  AND id = ?
3860
4141
  `
3861
4142
  )
3862
- .run(title, filename, content, updatedAt, normalizedDatabaseKey, existingDocument.id);
4143
+ .run(folderId, title, filename, content, updatedAt, normalizedDatabaseKey, existingDocument.id);
3863
4144
 
3864
4145
  return this.getDatabaseDocument(normalizedDatabaseKey, existingDocument.id);
3865
4146
  }
@@ -197,10 +197,15 @@ function isWritable(filePath) {
197
197
 
198
198
  function getFileMetadata(filePath) {
199
199
  const stat = fs.statSync(filePath);
200
+ const createdAt =
201
+ Number.isFinite(stat.birthtimeMs) && stat.birthtimeMs > 0
202
+ ? stat.birthtime
203
+ : stat.ctime;
200
204
 
201
205
  return {
202
206
  path: filePath,
203
207
  sizeBytes: stat.size,
208
+ createdAt: createdAt.toISOString(),
204
209
  lastModifiedAt: stat.mtime.toISOString(),
205
210
  };
206
211
  }
@@ -0,0 +1,49 @@
1
+ function getActiveConnection(connectionManager) {
2
+ try {
3
+ return connectionManager?.getActiveConnection?.() ?? null;
4
+ } catch {
5
+ return null;
6
+ }
7
+ }
8
+
9
+ function recordUserAction({
10
+ appStateStore,
11
+ connectionManager,
12
+ action,
13
+ targetType = "database",
14
+ targetName = null,
15
+ metadata = {},
16
+ databaseKey = null,
17
+ durationMs = null,
18
+ } = {}) {
19
+ if (!appStateStore?.recordAccessLog || !action) {
20
+ return null;
21
+ }
22
+
23
+ const activeConnection = getActiveConnection(connectionManager);
24
+ const resolvedDatabaseKey = databaseKey ?? activeConnection?.id ?? null;
25
+ const resolvedTargetName =
26
+ targetName ?? activeConnection?.label ?? activeConnection?.path ?? resolvedDatabaseKey ?? targetType;
27
+
28
+ try {
29
+ return appStateStore.recordAccessLog({
30
+ source: "user",
31
+ action,
32
+ databaseKey: resolvedDatabaseKey,
33
+ targetType,
34
+ targetName: resolvedTargetName,
35
+ status: "success",
36
+ durationMs,
37
+ metadata: {
38
+ databaseLabel: activeConnection?.label ?? null,
39
+ ...metadata,
40
+ },
41
+ });
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ module.exports = {
48
+ recordUserAction,
49
+ };