memory-journal-mcp 4.4.2 → 5.0.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.
- package/.github/workflows/codeql.yml +1 -6
- package/.github/workflows/docker-publish.yml +15 -49
- package/.github/workflows/lint-and-test.yml +1 -1
- package/.github/workflows/secrets-scanning.yml +4 -3
- package/.github/workflows/security-update.yml +3 -3
- package/CHANGELOG.md +213 -0
- package/CONTRIBUTING.md +132 -97
- package/DOCKER_README.md +184 -235
- package/Dockerfile +27 -24
- package/README.md +218 -190
- package/SECURITY.md +27 -35
- package/dist/cli.js +16 -1
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +5 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +133 -73
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -2
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +7 -6
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +37 -24
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +319 -157
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/database/schema.d.ts +45 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +92 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +13 -2
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +1 -3
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/github.d.ts +12 -0
- package/dist/handlers/prompts/github.d.ts.map +1 -0
- package/dist/handlers/prompts/github.js +178 -0
- package/dist/handlers/prompts/github.js.map +1 -0
- package/dist/handlers/prompts/index.d.ts +23 -2
- package/dist/handlers/prompts/index.d.ts.map +1 -1
- package/dist/handlers/prompts/index.js +7 -432
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/prompts/workflow.d.ts +12 -0
- package/dist/handlers/prompts/workflow.d.ts.map +1 -0
- package/dist/handlers/prompts/workflow.js +277 -0
- package/dist/handlers/prompts/workflow.js.map +1 -0
- package/dist/handlers/resources/core.d.ts +11 -0
- package/dist/handlers/resources/core.d.ts.map +1 -0
- package/dist/handlers/resources/core.js +433 -0
- package/dist/handlers/resources/core.js.map +1 -0
- package/dist/handlers/resources/github.d.ts +11 -0
- package/dist/handlers/resources/github.d.ts.map +1 -0
- package/dist/handlers/resources/github.js +314 -0
- package/dist/handlers/resources/github.js.map +1 -0
- package/dist/handlers/resources/graph.d.ts +11 -0
- package/dist/handlers/resources/graph.d.ts.map +1 -0
- package/dist/handlers/resources/graph.js +204 -0
- package/dist/handlers/resources/graph.js.map +1 -0
- package/dist/handlers/resources/index.d.ts +5 -20
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +16 -1278
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/resources/shared.d.ts +60 -0
- package/dist/handlers/resources/shared.d.ts.map +1 -0
- package/dist/handlers/resources/shared.js +49 -0
- package/dist/handlers/resources/shared.js.map +1 -0
- package/dist/handlers/resources/team.d.ts +13 -0
- package/dist/handlers/resources/team.d.ts.map +1 -0
- package/dist/handlers/resources/team.js +119 -0
- package/dist/handlers/resources/team.js.map +1 -0
- package/dist/handlers/resources/templates.d.ts +13 -0
- package/dist/handlers/resources/templates.d.ts.map +1 -0
- package/dist/handlers/resources/templates.js +310 -0
- package/dist/handlers/resources/templates.js.map +1 -0
- package/dist/handlers/tools/admin.d.ts +8 -0
- package/dist/handlers/tools/admin.d.ts.map +1 -0
- package/dist/handlers/tools/admin.js +270 -0
- package/dist/handlers/tools/admin.js.map +1 -0
- package/dist/handlers/tools/analytics.d.ts +8 -0
- package/dist/handlers/tools/analytics.d.ts.map +1 -0
- package/dist/handlers/tools/analytics.js +256 -0
- package/dist/handlers/tools/analytics.js.map +1 -0
- package/dist/handlers/tools/backup.d.ts +8 -0
- package/dist/handlers/tools/backup.d.ts.map +1 -0
- package/dist/handlers/tools/backup.js +224 -0
- package/dist/handlers/tools/backup.js.map +1 -0
- package/dist/handlers/tools/core.d.ts +9 -0
- package/dist/handlers/tools/core.d.ts.map +1 -0
- package/dist/handlers/tools/core.js +326 -0
- package/dist/handlers/tools/core.js.map +1 -0
- package/dist/handlers/tools/export.d.ts +8 -0
- package/dist/handlers/tools/export.d.ts.map +1 -0
- package/dist/handlers/tools/export.js +89 -0
- package/dist/handlers/tools/export.js.map +1 -0
- package/dist/handlers/tools/github/helpers.d.ts +34 -0
- package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
- package/dist/handlers/tools/github/helpers.js +52 -0
- package/dist/handlers/tools/github/helpers.js.map +1 -0
- package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
- package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/insights-tools.js +104 -0
- package/dist/handlers/tools/github/insights-tools.js.map +1 -0
- package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
- package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/issue-tools.js +359 -0
- package/dist/handlers/tools/github/issue-tools.js.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.js +108 -0
- package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.js +302 -0
- package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.js +15 -0
- package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
- package/dist/handlers/tools/github/read-tools.d.ts +8 -0
- package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/read-tools.js +260 -0
- package/dist/handlers/tools/github/read-tools.js.map +1 -0
- package/dist/handlers/tools/github/schemas.d.ts +467 -0
- package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/github/schemas.js +335 -0
- package/dist/handlers/tools/github/schemas.js.map +1 -0
- package/dist/handlers/tools/github.d.ts +14 -0
- package/dist/handlers/tools/github.d.ts.map +1 -0
- package/dist/handlers/tools/github.js +28 -0
- package/dist/handlers/tools/github.js.map +1 -0
- package/dist/handlers/tools/index.d.ts +15 -20
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +117 -2909
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/handlers/tools/relationships.d.ts +8 -0
- package/dist/handlers/tools/relationships.d.ts.map +1 -0
- package/dist/handlers/tools/relationships.js +308 -0
- package/dist/handlers/tools/relationships.js.map +1 -0
- package/dist/handlers/tools/schemas.d.ts +108 -0
- package/dist/handlers/tools/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/schemas.js +122 -0
- package/dist/handlers/tools/schemas.js.map +1 -0
- package/dist/handlers/tools/search.d.ts +8 -0
- package/dist/handlers/tools/search.d.ts.map +1 -0
- package/dist/handlers/tools/search.js +282 -0
- package/dist/handlers/tools/search.js.map +1 -0
- package/dist/handlers/tools/team.d.ts +11 -0
- package/dist/handlers/tools/team.d.ts.map +1 -0
- package/dist/handlers/tools/team.js +239 -0
- package/dist/handlers/tools/team.js.map +1 -0
- package/dist/server/McpServer.d.ts +4 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +48 -297
- package/dist/server/McpServer.js.map +1 -1
- package/dist/server/Scheduler.d.ts +91 -0
- package/dist/server/Scheduler.d.ts.map +1 -0
- package/dist/server/Scheduler.js +201 -0
- package/dist/server/Scheduler.js.map +1 -0
- package/dist/transports/http.d.ts +66 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +519 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/types/entities.d.ts +101 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/entities.js +5 -0
- package/dist/types/entities.js.map +1 -0
- package/dist/types/filtering.d.ts +34 -0
- package/dist/types/filtering.d.ts.map +1 -0
- package/dist/types/filtering.js +5 -0
- package/dist/types/filtering.js.map +1 -0
- package/dist/types/github.d.ts +166 -0
- package/dist/types/github.d.ts.map +1 -0
- package/dist/types/github.js +5 -0
- package/dist/types/github.js.map +1 -0
- package/dist/types/index.d.ts +35 -292
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/error-helpers.d.ts +37 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +47 -0
- package/dist/utils/error-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +6 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security-utils.d.ts +0 -21
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +0 -47
- package/dist/utils/security-utils.js.map +1 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +9 -32
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +11 -2
- package/hooks/README.md +107 -0
- package/hooks/cursor/hooks.json +10 -0
- package/hooks/cursor/memory-journal.mdc +22 -0
- package/hooks/cursor/session-end.sh +19 -0
- package/hooks/kilo-code/session-end-mode.json +11 -0
- package/hooks/kiro/session-end.md +13 -0
- package/mcp-config-example.json +1 -0
- package/package.json +11 -9
- package/playwright.config.ts +29 -0
- package/releases/v4.5.0.md +116 -0
- package/releases/v5.0.0.md +105 -0
- package/scripts/generate-server-instructions.ts +176 -0
- package/scripts/server-instructions-function-body.ts +77 -0
- package/server.json +3 -3
- package/src/cli.ts +45 -1
- package/src/constants/ServerInstructions.ts +133 -73
- package/src/constants/icons.ts +8 -7
- package/src/constants/server-instructions.md +268 -0
- package/src/database/SqliteAdapter.ts +358 -192
- package/src/database/schema.ts +125 -0
- package/src/filtering/ToolFilter.ts +13 -2
- package/src/github/GitHubIntegration.ts +1 -3
- package/src/handlers/prompts/github.ts +209 -0
- package/src/handlers/prompts/index.ts +10 -499
- package/src/handlers/prompts/workflow.ts +314 -0
- package/src/handlers/resources/core.ts +528 -0
- package/src/handlers/resources/github.ts +358 -0
- package/src/handlers/resources/graph.ts +254 -0
- package/src/handlers/resources/index.ts +23 -1570
- package/src/handlers/resources/shared.ts +103 -0
- package/src/handlers/resources/team.ts +133 -0
- package/src/handlers/resources/templates.ts +374 -0
- package/src/handlers/tools/admin.ts +285 -0
- package/src/handlers/tools/analytics.ts +301 -0
- package/src/handlers/tools/backup.ts +242 -0
- package/src/handlers/tools/core.ts +350 -0
- package/src/handlers/tools/export.ts +115 -0
- package/src/handlers/tools/github/helpers.ts +86 -0
- package/src/handlers/tools/github/insights-tools.ts +119 -0
- package/src/handlers/tools/github/issue-tools.ts +439 -0
- package/src/handlers/tools/github/kanban-tools.ts +134 -0
- package/src/handlers/tools/github/milestone-tools.ts +392 -0
- package/src/handlers/tools/github/mutation-tools.ts +17 -0
- package/src/handlers/tools/github/read-tools.ts +328 -0
- package/src/handlers/tools/github/schemas.ts +369 -0
- package/src/handlers/tools/github.ts +36 -0
- package/src/handlers/tools/index.ts +144 -3325
- package/src/handlers/tools/relationships.ts +358 -0
- package/src/handlers/tools/schemas.ts +132 -0
- package/src/handlers/tools/search.ts +343 -0
- package/src/handlers/tools/team.ts +273 -0
- package/src/server/McpServer.ts +63 -358
- package/src/server/Scheduler.ts +278 -0
- package/src/transports/http.ts +635 -0
- package/src/types/entities.ts +145 -0
- package/src/types/filtering.ts +54 -0
- package/src/types/github.ts +180 -0
- package/src/types/index.ts +67 -375
- package/src/utils/error-helpers.ts +52 -0
- package/src/utils/logger.ts +6 -3
- package/src/utils/security-utils.ts +0 -52
- package/src/vector/VectorSearchManager.ts +9 -33
- package/tests/constants/icons.test.ts +1 -2
- package/tests/constants/server-instructions.test.ts +30 -4
- package/tests/database/sqlite-adapter.test.ts +91 -7
- package/tests/e2e/auth.spec.ts +154 -0
- package/tests/e2e/health.spec.ts +63 -0
- package/tests/e2e/protocols.spec.ts +134 -0
- package/tests/e2e/resources.spec.ts +103 -0
- package/tests/e2e/scheduler.spec.ts +79 -0
- package/tests/e2e/security.spec.ts +91 -0
- package/tests/e2e/sessions.spec.ts +95 -0
- package/tests/e2e/stateless.spec.ts +121 -0
- package/tests/e2e/tools.spec.ts +111 -0
- package/tests/filtering/tool-filter.test.ts +46 -0
- package/tests/handlers/error-path-coverage.test.ts +324 -0
- package/tests/handlers/github-resource-handlers.test.ts +453 -0
- package/tests/handlers/github-tool-handlers.test.ts +899 -0
- package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
- package/tests/handlers/prompt-handlers.test.ts +40 -0
- package/tests/handlers/resource-handler-coverage.test.ts +181 -0
- package/tests/handlers/resource-handlers.test.ts +33 -9
- package/tests/handlers/search-tool-handlers.test.ts +272 -0
- package/tests/handlers/targeted-gap-closure.test.ts +387 -0
- package/tests/handlers/team-resource-handlers.test.ts +156 -0
- package/tests/handlers/team-tool-handlers.test.ts +301 -0
- package/tests/handlers/tool-handler-coverage.test.ts +469 -0
- package/tests/handlers/tool-handlers.test.ts +2 -2
- package/tests/security/sql-injection.test.ts +3 -54
- package/tests/server/mcp-server.test.ts +503 -8
- package/tests/server/scheduler.test.ts +400 -0
- package/tests/transports/http-transport.test.ts +620 -0
- package/tests/vector/vector-search-manager.test.ts +60 -0
- package/vitest.config.ts +4 -1
- package/.memory-journal-team.db +0 -0
- package/.vscode/settings.json +0 -84
|
@@ -8,82 +8,8 @@ import initSqlJs from 'sql.js';
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
|
-
import { validateDateFormatPattern } from '../utils/security-utils.js';
|
|
12
|
-
|
|
13
|
-
const SCHEMA_SQL = `
|
|
14
|
-
-- Main journal entries table
|
|
15
|
-
CREATE TABLE IF NOT EXISTS memory_journal (
|
|
16
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
-
entry_type TEXT NOT NULL,
|
|
18
|
-
content TEXT NOT NULL,
|
|
19
|
-
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
-
is_personal INTEGER DEFAULT 1,
|
|
21
|
-
significance_type TEXT,
|
|
22
|
-
auto_context TEXT,
|
|
23
|
-
deleted_at TEXT,
|
|
24
|
-
-- GitHub integration fields
|
|
25
|
-
project_number INTEGER,
|
|
26
|
-
project_owner TEXT,
|
|
27
|
-
issue_number INTEGER,
|
|
28
|
-
issue_url TEXT,
|
|
29
|
-
pr_number INTEGER,
|
|
30
|
-
pr_url TEXT,
|
|
31
|
-
pr_status TEXT,
|
|
32
|
-
workflow_run_id INTEGER,
|
|
33
|
-
workflow_name TEXT,
|
|
34
|
-
workflow_status TEXT
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
-- Tags table
|
|
38
|
-
CREATE TABLE IF NOT EXISTS tags (
|
|
39
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
-
name TEXT UNIQUE NOT NULL,
|
|
41
|
-
usage_count INTEGER DEFAULT 0
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
-- Junction table for entry-tag relationships
|
|
45
|
-
CREATE TABLE IF NOT EXISTS entry_tags (
|
|
46
|
-
entry_id INTEGER NOT NULL,
|
|
47
|
-
tag_id INTEGER NOT NULL,
|
|
48
|
-
PRIMARY KEY (entry_id, tag_id),
|
|
49
|
-
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
|
|
50
|
-
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
-- Relationships between entries
|
|
54
|
-
CREATE TABLE IF NOT EXISTS relationships (
|
|
55
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
-
from_entry_id INTEGER NOT NULL,
|
|
57
|
-
to_entry_id INTEGER NOT NULL,
|
|
58
|
-
relationship_type TEXT NOT NULL,
|
|
59
|
-
description TEXT,
|
|
60
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
-
FOREIGN KEY (from_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
|
|
62
|
-
FOREIGN KEY (to_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
-- Embeddings for vector search (stored as JSON for sql.js compatibility)
|
|
66
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
67
|
-
entry_id INTEGER PRIMARY KEY,
|
|
68
|
-
embedding TEXT NOT NULL,
|
|
69
|
-
model_name TEXT NOT NULL,
|
|
70
|
-
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
-- Indexes for performance
|
|
74
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_timestamp ON memory_journal(timestamp);
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_type ON memory_journal(entry_type);
|
|
76
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_personal ON memory_journal(is_personal);
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_deleted ON memory_journal(deleted_at);
|
|
78
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_project ON memory_journal(project_number);
|
|
79
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_issue ON memory_journal(issue_number);
|
|
80
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_pr ON memory_journal(pr_number);
|
|
81
|
-
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
|
|
82
|
-
CREATE INDEX IF NOT EXISTS idx_entry_tags_entry ON entry_tags(entry_id);
|
|
83
|
-
CREATE INDEX IF NOT EXISTS idx_entry_tags_tag ON entry_tags(tag_id);
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entry_id);
|
|
85
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entry_id);
|
|
86
|
-
`;
|
|
11
|
+
import { validateDateFormatPattern, sanitizeSearchQuery, assertNoPathTraversal, } from '../utils/security-utils.js';
|
|
12
|
+
import { SCHEMA_SQL, TEAM_SCHEMA_SQL } from './schema.js';
|
|
87
13
|
/**
|
|
88
14
|
* SQLite Database Adapter for Memory Journal using sql.js
|
|
89
15
|
*/
|
|
@@ -128,11 +54,174 @@ export class SqliteAdapter {
|
|
|
128
54
|
}
|
|
129
55
|
// Initialize schema
|
|
130
56
|
this.db.run(SCHEMA_SQL);
|
|
57
|
+
// Migrate existing databases that may lack newer columns
|
|
58
|
+
this.migrateSchema();
|
|
59
|
+
// Enable foreign key enforcement (SQLite disables by default)
|
|
60
|
+
// Required for ON DELETE CASCADE in entry_tags, relationships, embeddings
|
|
61
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
131
62
|
this.initialized = true;
|
|
132
63
|
logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath });
|
|
133
64
|
// Immediate flush after initialization to persist schema
|
|
134
65
|
this.flushSave();
|
|
135
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Migrate existing databases that may lack newer columns.
|
|
69
|
+
* Required because CREATE TABLE IF NOT EXISTS is a no-op on
|
|
70
|
+
* existing tables — columns added after initial creation are
|
|
71
|
+
* never added. This method checks for each expected column and
|
|
72
|
+
* runs ALTER TABLE as needed.
|
|
73
|
+
* Idempotent — safe to call on databases that already have all columns.
|
|
74
|
+
*/
|
|
75
|
+
migrateSchema() {
|
|
76
|
+
const db = this.ensureDb();
|
|
77
|
+
const tableInfo = db.exec('PRAGMA table_info(memory_journal)');
|
|
78
|
+
const columns = new Set((tableInfo[0]?.values ?? []).map((row) => String(row[1])));
|
|
79
|
+
const requiredColumns = [
|
|
80
|
+
{
|
|
81
|
+
name: 'significance_type',
|
|
82
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN significance_type TEXT',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'auto_context',
|
|
86
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN auto_context TEXT',
|
|
87
|
+
},
|
|
88
|
+
{ name: 'deleted_at', sql: 'ALTER TABLE memory_journal ADD COLUMN deleted_at TEXT' },
|
|
89
|
+
{
|
|
90
|
+
name: 'project_number',
|
|
91
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_number INTEGER',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'project_owner',
|
|
95
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_owner TEXT',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'issue_number',
|
|
99
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN issue_number INTEGER',
|
|
100
|
+
},
|
|
101
|
+
{ name: 'issue_url', sql: 'ALTER TABLE memory_journal ADD COLUMN issue_url TEXT' },
|
|
102
|
+
{ name: 'pr_number', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_number INTEGER' },
|
|
103
|
+
{ name: 'pr_url', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_url TEXT' },
|
|
104
|
+
{ name: 'pr_status', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_status TEXT' },
|
|
105
|
+
{
|
|
106
|
+
name: 'workflow_run_id',
|
|
107
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_run_id INTEGER',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'workflow_name',
|
|
111
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_name TEXT',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'workflow_status',
|
|
115
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_status TEXT',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const added = [];
|
|
119
|
+
for (const col of requiredColumns) {
|
|
120
|
+
if (!columns.has(col.name)) {
|
|
121
|
+
db.run(col.sql);
|
|
122
|
+
added.push(col.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Fix any tags with NULL usage_count (data repair from legacy DBs)
|
|
126
|
+
db.run('UPDATE tags SET usage_count = 0 WHERE usage_count IS NULL');
|
|
127
|
+
// Drop legacy FTS5 triggers from Python-era databases.
|
|
128
|
+
// sql.js WASM does not include FTS5; these triggers cause "no such module: fts5"
|
|
129
|
+
// on INSERT/UPDATE/DELETE operations. The TypeScript codebase uses LIKE queries.
|
|
130
|
+
// NOTE: We only drop triggers (regular objects). Dropping FTS5 virtual tables
|
|
131
|
+
// would also require the fts5 module, so we leave the inert shadow tables in place.
|
|
132
|
+
const dropped = [];
|
|
133
|
+
const triggers = db.exec("SELECT name FROM sqlite_master WHERE type = 'trigger' AND sql LIKE '%fts%'");
|
|
134
|
+
// Validate trigger names before interpolating into DDL (defense-in-depth)
|
|
135
|
+
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
136
|
+
for (const row of triggers[0]?.values ?? []) {
|
|
137
|
+
const name = String(row[0]);
|
|
138
|
+
if (!SAFE_IDENTIFIER_RE.test(name)) {
|
|
139
|
+
logger.warning('Skipping trigger with unsafe name during migration', {
|
|
140
|
+
module: 'SqliteAdapter',
|
|
141
|
+
triggerName: name,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
db.run(`DROP TRIGGER IF EXISTS "${name}"`);
|
|
146
|
+
dropped.push(`trigger:${name}`);
|
|
147
|
+
}
|
|
148
|
+
const changes = [...added.map((c) => `column:${c}`), ...dropped];
|
|
149
|
+
if (changes.length > 0) {
|
|
150
|
+
this.flushSave();
|
|
151
|
+
logger.info('Schema migrated', {
|
|
152
|
+
module: 'SqliteAdapter',
|
|
153
|
+
dbPath: this.dbPath,
|
|
154
|
+
changes,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Apply additional schema for team databases (adds author column).
|
|
160
|
+
* Also migrates legacy team DBs that may be missing columns from the
|
|
161
|
+
* current main schema (e.g. issue_number, pr_number added after v2).
|
|
162
|
+
* Idempotent — safe to call on databases that already have all columns.
|
|
163
|
+
*/
|
|
164
|
+
applyTeamSchema() {
|
|
165
|
+
const db = this.ensureDb();
|
|
166
|
+
const tableInfo = db.exec('PRAGMA table_info(memory_journal)');
|
|
167
|
+
const columns = new Set((tableInfo[0]?.values ?? []).map((row) => String(row[1])));
|
|
168
|
+
// Columns required by the current schema that legacy team DBs may lack
|
|
169
|
+
const requiredColumns = [
|
|
170
|
+
{
|
|
171
|
+
name: 'issue_number',
|
|
172
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN issue_number INTEGER',
|
|
173
|
+
},
|
|
174
|
+
{ name: 'issue_url', sql: 'ALTER TABLE memory_journal ADD COLUMN issue_url TEXT' },
|
|
175
|
+
{ name: 'pr_number', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_number INTEGER' },
|
|
176
|
+
{ name: 'pr_url', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_url TEXT' },
|
|
177
|
+
{ name: 'pr_status', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_status TEXT' },
|
|
178
|
+
{
|
|
179
|
+
name: 'workflow_run_id',
|
|
180
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_run_id INTEGER',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'workflow_name',
|
|
184
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_name TEXT',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'workflow_status',
|
|
188
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_status TEXT',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'project_number',
|
|
192
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_number INTEGER',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'project_owner',
|
|
196
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_owner TEXT',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'significance_type',
|
|
200
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN significance_type TEXT',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'auto_context',
|
|
204
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN auto_context TEXT',
|
|
205
|
+
},
|
|
206
|
+
{ name: 'deleted_at', sql: 'ALTER TABLE memory_journal ADD COLUMN deleted_at TEXT' },
|
|
207
|
+
{ name: 'author', sql: TEAM_SCHEMA_SQL.trim() },
|
|
208
|
+
];
|
|
209
|
+
const added = [];
|
|
210
|
+
for (const col of requiredColumns) {
|
|
211
|
+
if (!columns.has(col.name)) {
|
|
212
|
+
db.run(col.sql);
|
|
213
|
+
added.push(col.name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (added.length > 0) {
|
|
217
|
+
this.flushSave();
|
|
218
|
+
logger.info('Team schema migrated', {
|
|
219
|
+
module: 'SqliteAdapter',
|
|
220
|
+
dbPath: this.dbPath,
|
|
221
|
+
columnsAdded: added,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
136
225
|
/**
|
|
137
226
|
* Schedule a debounced save to disk.
|
|
138
227
|
* Batches rapid mutations into a single write after SAVE_DEBOUNCE_MS.
|
|
@@ -344,8 +433,7 @@ export class SqliteAdapter {
|
|
|
344
433
|
const result = db.exec(sql, params);
|
|
345
434
|
if (result.length === 0)
|
|
346
435
|
return [];
|
|
347
|
-
|
|
348
|
-
return (result[0]?.values ?? []).map((values) => this.rowToEntry(this.rowToObject(columns, values)));
|
|
436
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? []);
|
|
349
437
|
}
|
|
350
438
|
/**
|
|
351
439
|
* Get a page of active entries for batch processing (e.g., vector index rebuild).
|
|
@@ -356,8 +444,7 @@ export class SqliteAdapter {
|
|
|
356
444
|
const result = db.exec(`SELECT * FROM memory_journal WHERE deleted_at IS NULL ORDER BY id ASC LIMIT ? OFFSET ?`, [limit, offset]);
|
|
357
445
|
if (result.length === 0)
|
|
358
446
|
return [];
|
|
359
|
-
|
|
360
|
-
return (result[0]?.values ?? []).map((values) => this.rowToEntry(this.rowToObject(columns, values)));
|
|
447
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? []);
|
|
361
448
|
}
|
|
362
449
|
/**
|
|
363
450
|
* Get total count of active (non-deleted) entries.
|
|
@@ -438,9 +525,9 @@ export class SqliteAdapter {
|
|
|
438
525
|
const { limit = 10, isPersonal, projectNumber, issueNumber, prNumber } = options;
|
|
439
526
|
let sql = `
|
|
440
527
|
SELECT * FROM memory_journal
|
|
441
|
-
WHERE deleted_at IS NULL AND content LIKE ?
|
|
528
|
+
WHERE deleted_at IS NULL AND content LIKE ? ESCAPE '\\'
|
|
442
529
|
`;
|
|
443
|
-
const params = [`%${query}%`];
|
|
530
|
+
const params = [`%${sanitizeSearchQuery(query)}%`];
|
|
444
531
|
if (isPersonal !== undefined) {
|
|
445
532
|
sql += ` AND is_personal = ?`;
|
|
446
533
|
params.push(isPersonal ? 1 : 0);
|
|
@@ -462,8 +549,7 @@ export class SqliteAdapter {
|
|
|
462
549
|
const result = db.exec(sql, params);
|
|
463
550
|
if (result.length === 0)
|
|
464
551
|
return [];
|
|
465
|
-
|
|
466
|
-
return (result[0]?.values ?? []).map((values) => this.rowToEntry(this.rowToObject(columns, values)));
|
|
552
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? []);
|
|
467
553
|
}
|
|
468
554
|
/**
|
|
469
555
|
* Search by date range
|
|
@@ -471,14 +557,28 @@ export class SqliteAdapter {
|
|
|
471
557
|
searchByDateRange(startDate, endDate, options = {}) {
|
|
472
558
|
const db = this.ensureDb();
|
|
473
559
|
const { entryType, tags, isPersonal, projectNumber } = options;
|
|
474
|
-
let sql
|
|
475
|
-
SELECT DISTINCT m.* FROM memory_journal m
|
|
476
|
-
LEFT JOIN entry_tags et ON m.id = et.entry_id
|
|
477
|
-
LEFT JOIN tags t ON et.tag_id = t.id
|
|
478
|
-
WHERE m.deleted_at IS NULL
|
|
479
|
-
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
480
|
-
`;
|
|
560
|
+
let sql;
|
|
481
561
|
const params = [startDate, endDate + ' 23:59:59'];
|
|
562
|
+
// Only JOIN tag tables when filtering by tags (avoids DISTINCT overhead)
|
|
563
|
+
if (tags && tags.length > 0) {
|
|
564
|
+
sql = `
|
|
565
|
+
SELECT DISTINCT m.* FROM memory_journal m
|
|
566
|
+
JOIN entry_tags et ON m.id = et.entry_id
|
|
567
|
+
JOIN tags t ON et.tag_id = t.id
|
|
568
|
+
WHERE m.deleted_at IS NULL
|
|
569
|
+
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
570
|
+
`;
|
|
571
|
+
const placeholders = tags.map(() => '?').join(',');
|
|
572
|
+
sql += ` AND t.name IN (${placeholders})`;
|
|
573
|
+
params.push(...tags);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
sql = `
|
|
577
|
+
SELECT * FROM memory_journal m
|
|
578
|
+
WHERE m.deleted_at IS NULL
|
|
579
|
+
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
482
582
|
if (entryType) {
|
|
483
583
|
sql += ` AND m.entry_type = ?`;
|
|
484
584
|
params.push(entryType);
|
|
@@ -491,17 +591,12 @@ export class SqliteAdapter {
|
|
|
491
591
|
sql += ` AND m.project_number = ?`;
|
|
492
592
|
params.push(projectNumber);
|
|
493
593
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
sql += ` AND t.name IN (${placeholders})`;
|
|
497
|
-
params.push(...tags);
|
|
498
|
-
}
|
|
499
|
-
sql += ` ORDER BY m.timestamp DESC`;
|
|
594
|
+
sql += ` ORDER BY m.timestamp DESC LIMIT ?`;
|
|
595
|
+
params.push(options.limit ?? 500);
|
|
500
596
|
const result = db.exec(sql, params);
|
|
501
597
|
if (result.length === 0)
|
|
502
598
|
return [];
|
|
503
|
-
|
|
504
|
-
return (result[0]?.values ?? []).map((values) => this.rowToEntry(this.rowToObject(columns, values)));
|
|
599
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? []);
|
|
505
600
|
}
|
|
506
601
|
// =========================================================================
|
|
507
602
|
// Tag Operations
|
|
@@ -510,22 +605,26 @@ export class SqliteAdapter {
|
|
|
510
605
|
* Get or create tags and link to entry
|
|
511
606
|
*/
|
|
512
607
|
linkTagsToEntry(entryId, tagNames) {
|
|
608
|
+
if (tagNames.length === 0)
|
|
609
|
+
return;
|
|
513
610
|
const db = this.ensureDb();
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
611
|
+
// Batch: insert all tags in one statement
|
|
612
|
+
const insertPlaceholders = tagNames.map(() => '(?, 0)').join(', ');
|
|
613
|
+
db.run(`INSERT OR IGNORE INTO tags (name, usage_count) VALUES ${insertPlaceholders}`, tagNames);
|
|
614
|
+
// Batch: fetch all tag IDs in one query
|
|
615
|
+
const selectPlaceholders = tagNames.map(() => '?').join(', ');
|
|
616
|
+
const result = db.exec(`SELECT id, name FROM tags WHERE name IN (${selectPlaceholders})`, tagNames);
|
|
617
|
+
const tagIds = [];
|
|
618
|
+
for (const row of result[0]?.values ?? []) {
|
|
619
|
+
const tagId = row[0];
|
|
620
|
+
tagIds.push(tagId);
|
|
621
|
+
// Link tag to entry
|
|
622
|
+
db.run('INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?, ?)', [
|
|
623
|
+
entryId,
|
|
624
|
+
tagId,
|
|
625
|
+
]);
|
|
626
|
+
// Increment usage
|
|
627
|
+
db.run('UPDATE tags SET usage_count = usage_count + 1 WHERE id = ?', [tagId]);
|
|
529
628
|
}
|
|
530
629
|
}
|
|
531
630
|
/**
|
|
@@ -547,7 +646,7 @@ export class SqliteAdapter {
|
|
|
547
646
|
*/
|
|
548
647
|
listTags() {
|
|
549
648
|
const db = this.ensureDb();
|
|
550
|
-
const result = db.exec('SELECT
|
|
649
|
+
const result = db.exec('SELECT id, name, COALESCE(usage_count, 0) as usage_count FROM tags WHERE COALESCE(usage_count, 0) > 0 ORDER BY usage_count DESC');
|
|
551
650
|
if (result.length === 0)
|
|
552
651
|
return [];
|
|
553
652
|
return (result[0]?.values ?? []).map((v) => ({
|
|
@@ -682,55 +781,51 @@ export class SqliteAdapter {
|
|
|
682
781
|
*/
|
|
683
782
|
getStatistics(groupBy = 'week') {
|
|
684
783
|
const db = this.ensureDb();
|
|
685
|
-
//
|
|
686
|
-
const
|
|
687
|
-
SELECT COUNT(*) as count FROM memory_journal WHERE deleted_at IS NULL
|
|
688
|
-
`);
|
|
689
|
-
const totalEntries = totalResult[0]?.values[0]?.[0] ?? 0;
|
|
690
|
-
// By type
|
|
691
|
-
const byTypeResult = db.exec(`
|
|
784
|
+
// Combined query 1: total entries + breakdown by type
|
|
785
|
+
const combinedResult = db.exec(`
|
|
786
|
+
SELECT COUNT(*) as count FROM memory_journal WHERE deleted_at IS NULL;
|
|
692
787
|
SELECT entry_type, COUNT(*) as count
|
|
693
788
|
FROM memory_journal
|
|
694
789
|
WHERE deleted_at IS NULL
|
|
695
790
|
GROUP BY entry_type
|
|
696
791
|
`);
|
|
792
|
+
const totalEntries = combinedResult[0]?.values[0]?.[0] ?? 0;
|
|
697
793
|
const entriesByType = {};
|
|
698
|
-
for (const row of
|
|
794
|
+
for (const row of combinedResult[1]?.values ?? []) {
|
|
699
795
|
entriesByType[row[0]] = row[1];
|
|
700
796
|
}
|
|
701
|
-
//
|
|
797
|
+
// Combined query 2: period breakdown + decision density (using CASE)
|
|
702
798
|
const dateFormat = validateDateFormatPattern(groupBy);
|
|
703
|
-
const
|
|
704
|
-
SELECT
|
|
799
|
+
const periodResult = db.exec(`
|
|
800
|
+
SELECT
|
|
801
|
+
strftime('${dateFormat}', timestamp) as period,
|
|
802
|
+
COUNT(*) as total_count,
|
|
803
|
+
SUM(CASE WHEN significance_type IS NOT NULL THEN 1 ELSE 0 END) as significant_count
|
|
705
804
|
FROM memory_journal
|
|
706
805
|
WHERE deleted_at IS NULL
|
|
707
806
|
GROUP BY period
|
|
708
807
|
ORDER BY period DESC
|
|
709
808
|
LIMIT 52
|
|
710
809
|
`);
|
|
711
|
-
const entriesByPeriod = (
|
|
810
|
+
const entriesByPeriod = (periodResult[0]?.values ?? []).map((v) => ({
|
|
712
811
|
period: v[0],
|
|
713
812
|
count: v[1],
|
|
714
813
|
}));
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
// Decision Density: significant entries per period
|
|
719
|
-
const decisionDensityResult = db.exec(`
|
|
720
|
-
SELECT strftime('${dateFormat}', timestamp) as period, COUNT(*) as count
|
|
721
|
-
FROM memory_journal
|
|
722
|
-
WHERE deleted_at IS NULL AND significance_type IS NOT NULL
|
|
723
|
-
GROUP BY period
|
|
724
|
-
ORDER BY period DESC
|
|
725
|
-
LIMIT 52
|
|
726
|
-
`);
|
|
727
|
-
const decisionDensity = (decisionDensityResult[0]?.values ?? []).map((v) => ({
|
|
814
|
+
const decisionDensity = (periodResult[0]?.values ?? [])
|
|
815
|
+
.filter((v) => v[2] > 0)
|
|
816
|
+
.map((v) => ({
|
|
728
817
|
period: v[0],
|
|
729
|
-
significantCount: v[
|
|
818
|
+
significantCount: v[2],
|
|
730
819
|
}));
|
|
731
|
-
//
|
|
732
|
-
const
|
|
733
|
-
|
|
820
|
+
// Combined query 3: relationship counts + causal breakdown
|
|
821
|
+
const relResult = db.exec(`
|
|
822
|
+
SELECT COUNT(*) FROM relationships;
|
|
823
|
+
SELECT relationship_type, COUNT(*) as count
|
|
824
|
+
FROM relationships
|
|
825
|
+
WHERE relationship_type IN ('blocked_by', 'resolved', 'caused')
|
|
826
|
+
GROUP BY relationship_type
|
|
827
|
+
`);
|
|
828
|
+
const totalRelationships = relResult[0]?.values[0]?.[0] ?? 0;
|
|
734
829
|
const avgPerEntry = totalEntries > 0 ? totalRelationships / totalEntries : 0;
|
|
735
830
|
// Activity Trend: week-over-week growth
|
|
736
831
|
const currentPeriod = entriesByPeriod[0]?.period ?? '';
|
|
@@ -741,14 +836,8 @@ export class SqliteAdapter {
|
|
|
741
836
|
? Math.round(((currentCount - previousCount) / previousCount) * 100)
|
|
742
837
|
: null;
|
|
743
838
|
// Causal Metrics: counts for causal relationship types
|
|
744
|
-
const causalResult = db.exec(`
|
|
745
|
-
SELECT relationship_type, COUNT(*) as count
|
|
746
|
-
FROM relationships
|
|
747
|
-
WHERE relationship_type IN ('blocked_by', 'resolved', 'caused')
|
|
748
|
-
GROUP BY relationship_type
|
|
749
|
-
`);
|
|
750
839
|
const causalMetrics = { blocked_by: 0, resolved: 0, caused: 0 };
|
|
751
|
-
for (const row of
|
|
840
|
+
for (const row of relResult[1]?.values ?? []) {
|
|
752
841
|
const relType = row[0];
|
|
753
842
|
causalMetrics[relType] = row[1];
|
|
754
843
|
}
|
|
@@ -786,6 +875,10 @@ export class SqliteAdapter {
|
|
|
786
875
|
exportToFile(backupName) {
|
|
787
876
|
const db = this.ensureDb();
|
|
788
877
|
const backupsDir = this.getBackupsDir();
|
|
878
|
+
// Validate backup name against path traversal before sanitization
|
|
879
|
+
if (backupName) {
|
|
880
|
+
assertNoPathTraversal(backupName);
|
|
881
|
+
}
|
|
789
882
|
// Ensure backups directory exists
|
|
790
883
|
if (!fs.existsSync(backupsDir)) {
|
|
791
884
|
fs.mkdirSync(backupsDir, { recursive: true });
|
|
@@ -854,7 +947,7 @@ export class SqliteAdapter {
|
|
|
854
947
|
*/
|
|
855
948
|
deleteOldBackups(keepCount) {
|
|
856
949
|
const backups = this.listBackups(); // Already sorted newest-first
|
|
857
|
-
if (keepCount < 1) {
|
|
950
|
+
if (keepCount < 1 || Number.isNaN(keepCount)) {
|
|
858
951
|
throw new Error('keepCount must be at least 1');
|
|
859
952
|
}
|
|
860
953
|
const toKeep = backups.slice(0, keepCount);
|
|
@@ -883,9 +976,7 @@ export class SqliteAdapter {
|
|
|
883
976
|
*/
|
|
884
977
|
async restoreFromFile(filename) {
|
|
885
978
|
// Validate filename (prevent path traversal)
|
|
886
|
-
|
|
887
|
-
throw new Error('Invalid backup filename: path separators not allowed');
|
|
888
|
-
}
|
|
979
|
+
assertNoPathTraversal(filename);
|
|
889
980
|
const backupsDir = this.getBackupsDir();
|
|
890
981
|
const backupPath = path.join(backupsDir, filename);
|
|
891
982
|
if (!fs.existsSync(backupPath)) {
|
|
@@ -906,6 +997,7 @@ export class SqliteAdapter {
|
|
|
906
997
|
// Initialize new database from backup
|
|
907
998
|
const SQL = await import('sql.js').then((m) => m.default());
|
|
908
999
|
this.db = new SQL.Database(backupBuffer);
|
|
1000
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
909
1001
|
this.initialized = true;
|
|
910
1002
|
// Get new entry count
|
|
911
1003
|
const newCountResult = this.db.exec('SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NULL');
|
|
@@ -988,7 +1080,9 @@ export class SqliteAdapter {
|
|
|
988
1080
|
return obj;
|
|
989
1081
|
}
|
|
990
1082
|
/**
|
|
991
|
-
* Convert database row to JournalEntry
|
|
1083
|
+
* Convert database row to JournalEntry.
|
|
1084
|
+
* Used for single-entry methods (getEntryById, getEntryByIdIncludeDeleted)
|
|
1085
|
+
* where the N+1 overhead of one tag query is negligible.
|
|
992
1086
|
*/
|
|
993
1087
|
rowToEntry(row) {
|
|
994
1088
|
const id = row['id'];
|
|
@@ -1016,7 +1110,75 @@ export class SqliteAdapter {
|
|
|
1016
1110
|
};
|
|
1017
1111
|
}
|
|
1018
1112
|
/**
|
|
1019
|
-
*
|
|
1113
|
+
* Convert multiple database rows to JournalEntry[] with batch tag fetching.
|
|
1114
|
+
* Uses a single IN (...) query to fetch all tags for all entries at once,
|
|
1115
|
+
* eliminating the N+1 query pattern of per-row getTagsForEntry calls.
|
|
1116
|
+
*/
|
|
1117
|
+
rowsToEntries(columns, values) {
|
|
1118
|
+
if (values.length === 0)
|
|
1119
|
+
return [];
|
|
1120
|
+
// Step 1: Convert all rows to objects
|
|
1121
|
+
const rows = values.map((v) => this.rowToObject(columns, v));
|
|
1122
|
+
const ids = rows.map((r) => r['id']);
|
|
1123
|
+
// Step 2: Batch-fetch all tags in one query
|
|
1124
|
+
const tagMap = this.batchGetTagsForEntries(ids);
|
|
1125
|
+
// Step 3: Assemble entries using the pre-fetched tag map
|
|
1126
|
+
return rows.map((row) => {
|
|
1127
|
+
const id = row['id'];
|
|
1128
|
+
return {
|
|
1129
|
+
id,
|
|
1130
|
+
entryType: row['entry_type'],
|
|
1131
|
+
content: row['content'],
|
|
1132
|
+
timestamp: row['timestamp'],
|
|
1133
|
+
isPersonal: row['is_personal'] === 1,
|
|
1134
|
+
significanceType: row['significance_type'],
|
|
1135
|
+
autoContext: row['auto_context'],
|
|
1136
|
+
deletedAt: row['deleted_at'],
|
|
1137
|
+
tags: tagMap.get(id) ?? [],
|
|
1138
|
+
projectNumber: row['project_number'] ?? null,
|
|
1139
|
+
projectOwner: row['project_owner'] ?? null,
|
|
1140
|
+
issueNumber: row['issue_number'] ?? null,
|
|
1141
|
+
issueUrl: row['issue_url'] ?? null,
|
|
1142
|
+
prNumber: row['pr_number'] ?? null,
|
|
1143
|
+
prUrl: row['pr_url'] ?? null,
|
|
1144
|
+
prStatus: row['pr_status'] ?? null,
|
|
1145
|
+
workflowRunId: row['workflow_run_id'] ?? null,
|
|
1146
|
+
workflowName: row['workflow_name'] ?? null,
|
|
1147
|
+
workflowStatus: row['workflow_status'] ?? null,
|
|
1148
|
+
};
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Batch-fetch tags for multiple entry IDs in a single query.
|
|
1153
|
+
* Returns a Map<entryId, tagNames[]>.
|
|
1154
|
+
* Eliminates the N+1 query problem for multi-row result sets.
|
|
1155
|
+
*/
|
|
1156
|
+
batchGetTagsForEntries(ids) {
|
|
1157
|
+
const tagMap = new Map();
|
|
1158
|
+
if (ids.length === 0)
|
|
1159
|
+
return tagMap;
|
|
1160
|
+
const db = this.ensureDb();
|
|
1161
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
1162
|
+
const result = db.exec(`SELECT et.entry_id, t.name
|
|
1163
|
+
FROM entry_tags et
|
|
1164
|
+
JOIN tags t ON et.tag_id = t.id
|
|
1165
|
+
WHERE et.entry_id IN (${placeholders})`, ids);
|
|
1166
|
+
for (const row of result[0]?.values ?? []) {
|
|
1167
|
+
const entryId = row[0];
|
|
1168
|
+
const tagName = row[1];
|
|
1169
|
+
const existing = tagMap.get(entryId);
|
|
1170
|
+
if (existing) {
|
|
1171
|
+
existing.push(tagName);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
tagMap.set(entryId, [tagName]);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return tagMap;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Get raw sql.js Database handle for advanced queries.
|
|
1181
|
+
* @internal Callers MUST use parameterized queries — never concatenate user input into SQL.
|
|
1020
1182
|
*/
|
|
1021
1183
|
getRawDb() {
|
|
1022
1184
|
return this.ensureDb();
|