@novis10813/secondbrain-cli 0.1.0 → 0.1.1
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/README.md +74 -5
- package/dist/commands/backlinks.d.ts.map +1 -1
- package/dist/commands/backlinks.js +16 -33
- package/dist/commands/backlinks.js.map +1 -1
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +71 -57
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +5 -20
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/get.js +27 -26
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +21 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +22 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/open.d.ts +3 -0
- package/dist/commands/open.d.ts.map +1 -0
- package/dist/commands/open.js +28 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/orphans.d.ts.map +1 -1
- package/dist/commands/orphans.js +9 -27
- package/dist/commands/orphans.js.map +1 -1
- package/dist/commands/outlinks.d.ts +3 -0
- package/dist/commands/outlinks.d.ts.map +1 -0
- package/dist/commands/outlinks.js +48 -0
- package/dist/commands/outlinks.js.map +1 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +57 -39
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +4 -18
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +3 -17
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/template.d.ts +3 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +63 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/vault.d.ts +3 -0
- package/dist/commands/vault.d.ts.map +1 -0
- package/dist/commands/vault.js +233 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +134 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/database.d.ts +58 -14
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +627 -207
- package/dist/utils/database.js.map +1 -1
- package/dist/utils/global-config.d.ts +53 -0
- package/dist/utils/global-config.d.ts.map +1 -0
- package/dist/utils/global-config.js +144 -0
- package/dist/utils/global-config.js.map +1 -0
- package/dist/utils/parser.d.ts +95 -2
- package/dist/utils/parser.d.ts.map +1 -1
- package/dist/utils/parser.js +436 -38
- package/dist/utils/parser.js.map +1 -1
- package/dist/utils/placeholder.d.ts +37 -0
- package/dist/utils/placeholder.d.ts.map +1 -0
- package/dist/utils/placeholder.js +113 -0
- package/dist/utils/placeholder.js.map +1 -0
- package/dist/utils/position.d.ts +10 -0
- package/dist/utils/position.d.ts.map +1 -0
- package/dist/utils/position.js +24 -0
- package/dist/utils/position.js.map +1 -0
- package/dist/utils/sqlite-adapter.d.ts +8 -0
- package/dist/utils/sqlite-adapter.d.ts.map +1 -0
- package/dist/utils/sqlite-adapter.js +14 -0
- package/dist/utils/sqlite-adapter.js.map +1 -0
- package/dist/utils/template.d.ts +2 -2
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +8 -3
- package/dist/utils/template.js.map +1 -1
- package/dist/utils/vault-resolve.d.ts +23 -0
- package/dist/utils/vault-resolve.d.ts.map +1 -0
- package/dist/utils/vault-resolve.js +86 -0
- package/dist/utils/vault-resolve.js.map +1 -0
- package/dist/utils/vault.d.ts +77 -10
- package/dist/utils/vault.d.ts.map +1 -1
- package/dist/utils/vault.js +253 -98
- package/dist/utils/vault.js.map +1 -1
- package/package.json +9 -3
package/dist/utils/database.js
CHANGED
|
@@ -1,261 +1,681 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DatabaseManager = void 0;
|
|
4
|
-
const
|
|
4
|
+
const sqlite_adapter_js_1 = require("./sqlite-adapter.js");
|
|
5
5
|
class DatabaseManager {
|
|
6
6
|
db;
|
|
7
7
|
config;
|
|
8
8
|
constructor(config) {
|
|
9
9
|
this.config = config;
|
|
10
|
-
this.db =
|
|
10
|
+
this.db = (0, sqlite_adapter_js_1.createDatabase)(config.dbPath);
|
|
11
|
+
this.db.exec('PRAGMA foreign_keys = ON');
|
|
11
12
|
this.initTables();
|
|
12
13
|
}
|
|
13
14
|
initTables() {
|
|
14
|
-
//
|
|
15
|
+
// Drop legacy tables if they exist (breaking change - users need to re-sync)
|
|
15
16
|
this.db.exec(`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
DROP TABLE IF EXISTS links;
|
|
18
|
+
DROP TABLE IF EXISTS notes;
|
|
19
|
+
`);
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)
|
|
22
|
+
`);
|
|
23
|
+
const versionRow = this.db.prepare('SELECT version FROM schema_version LIMIT 1').get();
|
|
24
|
+
const currentVersion = versionRow?.version ?? 0;
|
|
25
|
+
if (currentVersion === 0) {
|
|
26
|
+
this.db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(0);
|
|
27
|
+
}
|
|
28
|
+
// New Obsidian-aligned tables
|
|
29
|
+
this.initObsidianTables();
|
|
30
|
+
}
|
|
31
|
+
initObsidianTables() {
|
|
32
|
+
// Files table (FileInfo/TFile equivalent)
|
|
33
|
+
this.db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
35
|
+
path TEXT PRIMARY KEY,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
basename TEXT NOT NULL,
|
|
38
|
+
extension TEXT NOT NULL,
|
|
39
|
+
parent TEXT,
|
|
40
|
+
ctime INTEGER NOT NULL,
|
|
41
|
+
mtime INTEGER NOT NULL,
|
|
42
|
+
size INTEGER NOT NULL,
|
|
43
|
+
content_hash TEXT NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
// Content metadata table
|
|
47
|
+
this.db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS content_metadata (
|
|
49
|
+
file_path TEXT PRIMARY KEY,
|
|
50
|
+
content_hash TEXT NOT NULL,
|
|
51
|
+
frontmatter_start_line INTEGER,
|
|
52
|
+
frontmatter_start_col INTEGER,
|
|
53
|
+
frontmatter_start_offset INTEGER,
|
|
54
|
+
frontmatter_end_line INTEGER,
|
|
55
|
+
frontmatter_end_col INTEGER,
|
|
56
|
+
frontmatter_end_offset INTEGER,
|
|
57
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
// Links table with positions (new structure)
|
|
61
|
+
this.db.exec(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS links_with_positions (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
source_path TEXT NOT NULL,
|
|
65
|
+
target_path TEXT,
|
|
66
|
+
target_id TEXT,
|
|
67
|
+
link_target TEXT, -- Original link target (e.g. "Note Name")
|
|
68
|
+
original TEXT NOT NULL,
|
|
69
|
+
display_text TEXT,
|
|
70
|
+
start_line INTEGER NOT NULL,
|
|
71
|
+
start_col INTEGER NOT NULL,
|
|
72
|
+
start_offset INTEGER NOT NULL,
|
|
73
|
+
end_line INTEGER NOT NULL,
|
|
74
|
+
end_col INTEGER NOT NULL,
|
|
75
|
+
end_offset INTEGER NOT NULL,
|
|
76
|
+
FOREIGN KEY (source_path) REFERENCES files(path) ON DELETE CASCADE
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
// Tags table with positions
|
|
80
|
+
this.db.exec(`
|
|
81
|
+
CREATE TABLE IF NOT EXISTS tags_with_positions (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
file_path TEXT NOT NULL,
|
|
84
|
+
tag TEXT NOT NULL,
|
|
85
|
+
start_line INTEGER NOT NULL,
|
|
86
|
+
start_col INTEGER NOT NULL,
|
|
87
|
+
start_offset INTEGER NOT NULL,
|
|
88
|
+
end_line INTEGER NOT NULL,
|
|
89
|
+
end_col INTEGER NOT NULL,
|
|
90
|
+
end_offset INTEGER NOT NULL,
|
|
91
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
26
92
|
)
|
|
27
93
|
`);
|
|
28
|
-
//
|
|
94
|
+
// Headings table
|
|
29
95
|
this.db.exec(`
|
|
30
|
-
CREATE TABLE IF NOT EXISTS
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
96
|
+
CREATE TABLE IF NOT EXISTS headings_with_positions (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
file_path TEXT NOT NULL,
|
|
99
|
+
heading TEXT NOT NULL,
|
|
100
|
+
level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 6),
|
|
101
|
+
start_line INTEGER NOT NULL,
|
|
102
|
+
start_col INTEGER NOT NULL,
|
|
103
|
+
start_offset INTEGER NOT NULL,
|
|
104
|
+
end_line INTEGER NOT NULL,
|
|
105
|
+
end_col INTEGER NOT NULL,
|
|
106
|
+
end_offset INTEGER NOT NULL,
|
|
107
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
36
108
|
)
|
|
37
109
|
`);
|
|
38
|
-
//
|
|
110
|
+
// Blocks table
|
|
39
111
|
this.db.exec(`
|
|
40
|
-
CREATE
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
CREATE TABLE IF NOT EXISTS blocks_with_positions (
|
|
113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
+
file_path TEXT NOT NULL,
|
|
115
|
+
block_id TEXT NOT NULL,
|
|
116
|
+
start_line INTEGER NOT NULL,
|
|
117
|
+
start_col INTEGER NOT NULL,
|
|
118
|
+
start_offset INTEGER NOT NULL,
|
|
119
|
+
end_line INTEGER NOT NULL,
|
|
120
|
+
end_col INTEGER NOT NULL,
|
|
121
|
+
end_offset INTEGER NOT NULL,
|
|
122
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE,
|
|
123
|
+
UNIQUE(file_path, block_id)
|
|
124
|
+
)
|
|
125
|
+
`);
|
|
126
|
+
// Embeds table
|
|
127
|
+
this.db.exec(`
|
|
128
|
+
CREATE TABLE IF NOT EXISTS embeds_with_positions (
|
|
129
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
130
|
+
file_path TEXT NOT NULL,
|
|
131
|
+
target_path TEXT NOT NULL,
|
|
132
|
+
original TEXT NOT NULL,
|
|
133
|
+
display_text TEXT,
|
|
134
|
+
start_line INTEGER NOT NULL,
|
|
135
|
+
start_col INTEGER NOT NULL,
|
|
136
|
+
start_offset INTEGER NOT NULL,
|
|
137
|
+
end_line INTEGER NOT NULL,
|
|
138
|
+
end_col INTEGER NOT NULL,
|
|
139
|
+
end_offset INTEGER NOT NULL,
|
|
140
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
141
|
+
)
|
|
142
|
+
`);
|
|
143
|
+
// Sections table (document sections: frontmatter, heading-bounded regions)
|
|
144
|
+
this.db.exec(`
|
|
145
|
+
CREATE TABLE IF NOT EXISTS sections_with_positions (
|
|
146
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
+
file_path TEXT NOT NULL,
|
|
148
|
+
section_id TEXT NOT NULL,
|
|
149
|
+
type TEXT NOT NULL,
|
|
150
|
+
start_line INTEGER NOT NULL,
|
|
151
|
+
start_col INTEGER NOT NULL,
|
|
152
|
+
start_offset INTEGER NOT NULL,
|
|
153
|
+
end_line INTEGER NOT NULL,
|
|
154
|
+
end_col INTEGER NOT NULL,
|
|
155
|
+
end_offset INTEGER NOT NULL,
|
|
156
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
157
|
+
)
|
|
158
|
+
`);
|
|
159
|
+
// Create indexes for new tables
|
|
160
|
+
this.db.exec(`
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_files_content_hash ON files(content_hash);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_files_basename ON files(basename COLLATE NOCASE);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_links_pos_source ON links_with_positions(source_path);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_links_pos_target ON links_with_positions(target_path);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_tags_pos_file ON tags_with_positions(file_path);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_tags_pos_tag ON tags_with_positions(tag);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_headings_pos_file ON headings_with_positions(file_path);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_blocks_pos_file ON blocks_with_positions(file_path);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_embeds_pos_file ON embeds_with_positions(file_path);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_sections_pos_file ON sections_with_positions(file_path);
|
|
44
171
|
`);
|
|
45
172
|
}
|
|
46
173
|
close() {
|
|
47
174
|
this.db.close();
|
|
48
175
|
}
|
|
49
|
-
|
|
50
|
-
|
|
176
|
+
rowToFileInfo(row) {
|
|
177
|
+
return {
|
|
178
|
+
path: row.path,
|
|
179
|
+
name: row.name,
|
|
180
|
+
basename: row.basename,
|
|
181
|
+
extension: row.extension,
|
|
182
|
+
parent: row.parent,
|
|
183
|
+
stat: { ctime: row.ctime, mtime: row.mtime, size: row.size }
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// FileInfo operations
|
|
187
|
+
upsertFile(file, contentHash) {
|
|
51
188
|
const stmt = this.db.prepare(`
|
|
52
|
-
INSERT INTO
|
|
189
|
+
INSERT INTO files (path, name, basename, extension, parent, ctime, mtime, size, content_hash)
|
|
53
190
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
191
|
ON CONFLICT(path) DO UPDATE SET
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
192
|
+
name = excluded.name,
|
|
193
|
+
basename = excluded.basename,
|
|
194
|
+
extension = excluded.extension,
|
|
195
|
+
parent = excluded.parent,
|
|
196
|
+
ctime = excluded.ctime,
|
|
197
|
+
mtime = excluded.mtime,
|
|
198
|
+
size = excluded.size,
|
|
199
|
+
content_hash = excluded.content_hash
|
|
62
200
|
`);
|
|
63
|
-
stmt.run(
|
|
64
|
-
// Update links
|
|
65
|
-
this.updateLinks(note.id, note.links);
|
|
201
|
+
stmt.run(file.path, file.name, file.basename, file.extension, file.parent || null, file.stat.ctime, file.stat.mtime, file.stat.size, contentHash || file.content_hash);
|
|
66
202
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
203
|
+
getFileByPath(path) {
|
|
204
|
+
const row = this.db.prepare('SELECT * FROM files WHERE path = ?').get(path);
|
|
205
|
+
return row ? this.rowToFileInfo(row) : null;
|
|
206
|
+
}
|
|
207
|
+
getFileByBasename(basename) {
|
|
208
|
+
const row = this.db.prepare('SELECT * FROM files WHERE basename = ? COLLATE NOCASE LIMIT 1').get(basename);
|
|
209
|
+
return row ? this.rowToFileInfo(row) : null;
|
|
210
|
+
}
|
|
211
|
+
deleteFile(path) {
|
|
212
|
+
this.db.prepare('DELETE FROM files WHERE path = ?').run(path);
|
|
213
|
+
}
|
|
214
|
+
getAllFiles() {
|
|
215
|
+
const rows = this.db.prepare('SELECT * FROM files').all();
|
|
216
|
+
return rows.map((row) => this.rowToFileInfo(row));
|
|
217
|
+
}
|
|
218
|
+
getFileContentHash(filePath) {
|
|
219
|
+
const row = this.db.prepare('SELECT content_hash FROM files WHERE path = ?').get(filePath);
|
|
220
|
+
return row?.content_hash ?? null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Update link target in links_with_positions table.
|
|
224
|
+
* Finds link by source_path and start_offset, then updates target_path and target_id.
|
|
225
|
+
*/
|
|
226
|
+
updateLinkTarget(sourcePath, startOffset, targetPath, targetId) {
|
|
227
|
+
const stmt = this.db.prepare(`
|
|
228
|
+
UPDATE links_with_positions
|
|
229
|
+
SET target_path = ?, target_id = ?
|
|
230
|
+
WHERE source_path = ? AND start_offset = ?
|
|
231
|
+
`);
|
|
232
|
+
stmt.run(targetPath, targetId, sourcePath, startOffset);
|
|
233
|
+
}
|
|
234
|
+
/** Get files that link to the given file path (new structure). */
|
|
235
|
+
getBacklinksByPath(filePath) {
|
|
236
|
+
const rows = this.db
|
|
237
|
+
.prepare(`
|
|
238
|
+
SELECT f.path, f.name, f.basename, f.extension, f.parent, f.ctime, f.mtime, f.size
|
|
239
|
+
FROM files f
|
|
240
|
+
INNER JOIN (SELECT DISTINCT source_path FROM links_with_positions WHERE target_path = ?) l
|
|
241
|
+
ON f.path = l.source_path
|
|
242
|
+
`)
|
|
243
|
+
.all(filePath);
|
|
244
|
+
return rows.map((row) => this.rowToFileInfo(row));
|
|
245
|
+
}
|
|
246
|
+
/** Get files that the given file links to (outgoing links, new structure). */
|
|
247
|
+
getOutlinksByPath(filePath) {
|
|
248
|
+
const rows = this.db
|
|
249
|
+
.prepare(`
|
|
250
|
+
SELECT f.path, f.name, f.basename, f.extension, f.parent, f.ctime, f.mtime, f.size
|
|
251
|
+
FROM files f
|
|
252
|
+
INNER JOIN (
|
|
253
|
+
SELECT DISTINCT target_path FROM links_with_positions
|
|
254
|
+
WHERE source_path = ? AND target_path IS NOT NULL AND target_path != ''
|
|
255
|
+
) l ON f.path = l.target_path
|
|
256
|
+
`)
|
|
257
|
+
.all(filePath);
|
|
258
|
+
return rows.map((row) => this.rowToFileInfo(row));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get start position of a heading in a file. Matches by exact heading text or Obsidian-style slug.
|
|
262
|
+
* @returns { line, col } (1-based) or null
|
|
263
|
+
*/
|
|
264
|
+
getHeadingPosition(filePath, headingFragment) {
|
|
265
|
+
const slug = (s) => s.toLowerCase().trim().replace(/\s+/g, '-');
|
|
266
|
+
const fragmentSlug = slug(headingFragment);
|
|
267
|
+
const rows = this.db.prepare('SELECT heading, start_line, start_col FROM headings_with_positions WHERE file_path = ?').all(filePath);
|
|
268
|
+
for (const row of rows) {
|
|
269
|
+
if (row.heading === headingFragment || slug(row.heading) === fragmentSlug) {
|
|
270
|
+
return { line: row.start_line, col: row.start_col };
|
|
99
271
|
}
|
|
100
272
|
}
|
|
273
|
+
return null;
|
|
101
274
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Get start position of a block (^block-id) in a file.
|
|
277
|
+
* @returns { line, col } (1-based) or null
|
|
278
|
+
*/
|
|
279
|
+
getBlockPosition(filePath, blockId) {
|
|
280
|
+
const row = this.db.prepare('SELECT start_line, start_col FROM blocks_with_positions WHERE file_path = ? AND block_id = ?').get(filePath, blockId);
|
|
281
|
+
return row ? { line: row.start_line, col: row.start_col } : null;
|
|
107
282
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return this.rowToNote(row);
|
|
283
|
+
/** Get files with no incoming or outgoing links (new structure). */
|
|
284
|
+
getOrphanFiles() {
|
|
285
|
+
const rows = this.db
|
|
286
|
+
.prepare(`
|
|
287
|
+
SELECT * FROM files f
|
|
288
|
+
WHERE NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.source_path = f.path)
|
|
289
|
+
AND NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.target_path = f.path)
|
|
290
|
+
`)
|
|
291
|
+
.all();
|
|
292
|
+
return rows.map((row) => this.rowToFileInfo(row));
|
|
119
293
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
294
|
+
/** Search files by path/basename, optional tags, path prefix, links-to target, heading, or mtime (new structure). */
|
|
295
|
+
searchFiles(query, tags, limit = 20, pathPrefix, linksToPath, headingQuery, modifiedAfter, modifiedBefore) {
|
|
296
|
+
const like = `%${query}%`;
|
|
297
|
+
let sql = `
|
|
298
|
+
SELECT f.*, (SELECT group_concat(DISTINCT tag) FROM tags_with_positions WHERE file_path = f.path) AS tags_str
|
|
299
|
+
FROM files f
|
|
300
|
+
WHERE (f.path LIKE ? OR f.basename LIKE ?)
|
|
301
|
+
`;
|
|
302
|
+
const params = [like, like];
|
|
303
|
+
if (pathPrefix !== undefined && pathPrefix !== '') {
|
|
304
|
+
sql += ` AND (f.path LIKE ? OR f.parent = ?)`;
|
|
305
|
+
params.push(`${pathPrefix}%`, pathPrefix);
|
|
306
|
+
}
|
|
123
307
|
if (tags && tags.length > 0) {
|
|
124
|
-
sql +=
|
|
125
|
-
params.push(...tags
|
|
308
|
+
sql += ` AND f.path IN (SELECT file_path FROM tags_with_positions WHERE tag IN (${tags.map(() => '?').join(',')}))`;
|
|
309
|
+
params.push(...tags);
|
|
310
|
+
}
|
|
311
|
+
if (linksToPath !== undefined && linksToPath !== '') {
|
|
312
|
+
sql += ` AND f.path IN (SELECT source_path FROM links_with_positions WHERE target_path = ?)`;
|
|
313
|
+
params.push(linksToPath);
|
|
314
|
+
}
|
|
315
|
+
if (headingQuery !== undefined && headingQuery !== '') {
|
|
316
|
+
sql += ` AND f.path IN (SELECT file_path FROM headings_with_positions WHERE heading LIKE ?)`;
|
|
317
|
+
params.push(`%${headingQuery}%`);
|
|
318
|
+
}
|
|
319
|
+
if (modifiedAfter !== undefined && Number.isFinite(modifiedAfter)) {
|
|
320
|
+
sql += ` AND f.mtime >= ?`;
|
|
321
|
+
params.push(modifiedAfter);
|
|
126
322
|
}
|
|
127
|
-
|
|
323
|
+
if (modifiedBefore !== undefined && Number.isFinite(modifiedBefore)) {
|
|
324
|
+
sql += ` AND f.mtime <= ?`;
|
|
325
|
+
params.push(modifiedBefore);
|
|
326
|
+
}
|
|
327
|
+
sql += ` ORDER BY f.mtime DESC LIMIT ?`;
|
|
128
328
|
params.push(limit);
|
|
129
329
|
const rows = this.db.prepare(sql).all(...params);
|
|
130
|
-
return
|
|
330
|
+
return rows.map((row) => ({
|
|
331
|
+
file: this.rowToFileInfo(row),
|
|
332
|
+
tags: row.tags_str ? row.tags_str.split(',') : []
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
// ContentMetadata operations
|
|
336
|
+
// ContentMetadata operations
|
|
337
|
+
upsertContentMetadata(filePath, metadata, contentHash) {
|
|
338
|
+
const transaction = this.db.transaction(() => {
|
|
339
|
+
this.doUpsertContentMetadata(filePath, metadata, contentHash);
|
|
340
|
+
});
|
|
341
|
+
transaction();
|
|
342
|
+
}
|
|
343
|
+
doUpsertContentMetadata(filePath, metadata, contentHash) {
|
|
344
|
+
// Prepare statements inside (potentially) outer transaction
|
|
345
|
+
const metadataStmt = this.db.prepare(`
|
|
346
|
+
INSERT INTO content_metadata (
|
|
347
|
+
file_path, content_hash,
|
|
348
|
+
frontmatter_start_line, frontmatter_start_col, frontmatter_start_offset,
|
|
349
|
+
frontmatter_end_line, frontmatter_end_col, frontmatter_end_offset
|
|
350
|
+
)
|
|
351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
352
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
353
|
+
content_hash = excluded.content_hash,
|
|
354
|
+
frontmatter_start_line = excluded.frontmatter_start_line,
|
|
355
|
+
frontmatter_start_col = excluded.frontmatter_start_col,
|
|
356
|
+
frontmatter_start_offset = excluded.frontmatter_start_offset,
|
|
357
|
+
frontmatter_end_line = excluded.frontmatter_end_line,
|
|
358
|
+
frontmatter_end_col = excluded.frontmatter_end_col,
|
|
359
|
+
frontmatter_end_offset = excluded.frontmatter_end_offset
|
|
360
|
+
`);
|
|
361
|
+
const deleteLinksStmt = this.db.prepare('DELETE FROM links_with_positions WHERE source_path = ?');
|
|
362
|
+
const deleteTagsStmt = this.db.prepare('DELETE FROM tags_with_positions WHERE file_path = ?');
|
|
363
|
+
const deleteHeadingsStmt = this.db.prepare('DELETE FROM headings_with_positions WHERE file_path = ?');
|
|
364
|
+
const deleteBlocksStmt = this.db.prepare('DELETE FROM blocks_with_positions WHERE file_path = ?');
|
|
365
|
+
const deleteEmbedsStmt = this.db.prepare('DELETE FROM embeds_with_positions WHERE file_path = ?');
|
|
366
|
+
const deleteSectionsStmt = this.db.prepare('DELETE FROM sections_with_positions WHERE file_path = ?');
|
|
367
|
+
const frontmatter = metadata.frontmatter;
|
|
368
|
+
metadataStmt.run(filePath, contentHash, frontmatter?.position.start.line ?? null, frontmatter?.position.start.col ?? null, frontmatter?.position.start.offset ?? null, frontmatter?.position.end.line ?? null, frontmatter?.position.end.col ?? null, frontmatter?.position.end.offset ?? null);
|
|
369
|
+
// Delete existing metadata items
|
|
370
|
+
deleteLinksStmt.run(filePath);
|
|
371
|
+
deleteTagsStmt.run(filePath);
|
|
372
|
+
deleteHeadingsStmt.run(filePath);
|
|
373
|
+
deleteBlocksStmt.run(filePath);
|
|
374
|
+
deleteEmbedsStmt.run(filePath);
|
|
375
|
+
deleteSectionsStmt.run(filePath);
|
|
376
|
+
// Prepare INSERT statements
|
|
377
|
+
const linkStmt = this.db.prepare(`
|
|
378
|
+
INSERT INTO links_with_positions (
|
|
379
|
+
source_path, target_path, target_id, link_target, original, display_text,
|
|
380
|
+
start_line, start_col, start_offset,
|
|
381
|
+
end_line, end_col, end_offset
|
|
382
|
+
)
|
|
383
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
384
|
+
`);
|
|
385
|
+
const tagStmt = this.db.prepare(`
|
|
386
|
+
INSERT INTO tags_with_positions (
|
|
387
|
+
file_path, tag,
|
|
388
|
+
start_line, start_col, start_offset,
|
|
389
|
+
end_line, end_col, end_offset
|
|
390
|
+
)
|
|
391
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
392
|
+
`);
|
|
393
|
+
const headingStmt = this.db.prepare(`
|
|
394
|
+
INSERT INTO headings_with_positions (
|
|
395
|
+
file_path, heading, level,
|
|
396
|
+
start_line, start_col, start_offset,
|
|
397
|
+
end_line, end_col, end_offset
|
|
398
|
+
)
|
|
399
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
400
|
+
`);
|
|
401
|
+
const blockStmt = this.db.prepare(`
|
|
402
|
+
INSERT INTO blocks_with_positions (
|
|
403
|
+
file_path, block_id,
|
|
404
|
+
start_line, start_col, start_offset,
|
|
405
|
+
end_line, end_col, end_offset
|
|
406
|
+
)
|
|
407
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
408
|
+
`);
|
|
409
|
+
const embedStmt = this.db.prepare(`
|
|
410
|
+
INSERT INTO embeds_with_positions (
|
|
411
|
+
file_path, target_path, original, display_text,
|
|
412
|
+
start_line, start_col, start_offset,
|
|
413
|
+
end_line, end_col, end_offset
|
|
414
|
+
)
|
|
415
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
416
|
+
`);
|
|
417
|
+
const sectionStmt = this.db.prepare(`
|
|
418
|
+
INSERT INTO sections_with_positions (
|
|
419
|
+
file_path, section_id, type,
|
|
420
|
+
start_line, start_col, start_offset,
|
|
421
|
+
end_line, end_col, end_offset
|
|
422
|
+
)
|
|
423
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
424
|
+
`);
|
|
425
|
+
// Insert links
|
|
426
|
+
if (metadata.links && metadata.links.length > 0) {
|
|
427
|
+
for (const link of metadata.links) {
|
|
428
|
+
linkStmt.run(filePath, link.link, null, link.link, link.original, link.displayText ?? null, link.position.start.line, link.position.start.col, link.position.start.offset, link.position.end.line, link.position.end.col, link.position.end.offset);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Insert tags
|
|
432
|
+
if (metadata.tags && metadata.tags.length > 0) {
|
|
433
|
+
for (const tag of metadata.tags) {
|
|
434
|
+
tagStmt.run(filePath, tag.tag, tag.position.start.line, tag.position.start.col, tag.position.start.offset, tag.position.end.line, tag.position.end.col, tag.position.end.offset);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Insert headings
|
|
438
|
+
if (metadata.headings && metadata.headings.length > 0) {
|
|
439
|
+
for (const heading of metadata.headings) {
|
|
440
|
+
headingStmt.run(filePath, heading.heading, heading.level, heading.position.start.line, heading.position.start.col, heading.position.start.offset, heading.position.end.line, heading.position.end.col, heading.position.end.offset);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Insert blocks
|
|
444
|
+
if (metadata.blocks && metadata.blocks.length > 0) {
|
|
445
|
+
for (const block of metadata.blocks) {
|
|
446
|
+
blockStmt.run(filePath, block.id, block.position.start.line, block.position.start.col, block.position.start.offset, block.position.end.line, block.position.end.col, block.position.end.offset);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Insert embeds
|
|
450
|
+
if (metadata.embeds && metadata.embeds.length > 0) {
|
|
451
|
+
for (const embed of metadata.embeds) {
|
|
452
|
+
embedStmt.run(filePath, embed.link, embed.original, embed.displayText ?? null, embed.position.start.line, embed.position.start.col, embed.position.start.offset, embed.position.end.line, embed.position.end.col, embed.position.end.offset);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Insert sections
|
|
456
|
+
if (metadata.sections && metadata.sections.length > 0) {
|
|
457
|
+
for (const section of metadata.sections) {
|
|
458
|
+
sectionStmt.run(filePath, section.id, section.type, section.position.start.line, section.position.start.col, section.position.start.offset, section.position.end.line, section.position.end.col, section.position.end.offset);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
131
461
|
}
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
462
|
+
getContentMetadata(filePath) {
|
|
463
|
+
const metadataRow = this.db.prepare('SELECT * FROM content_metadata WHERE file_path = ?').get(filePath);
|
|
464
|
+
if (!metadataRow)
|
|
465
|
+
return null;
|
|
466
|
+
const result = {};
|
|
467
|
+
// Frontmatter
|
|
468
|
+
if (metadataRow.frontmatter_start_line != null &&
|
|
469
|
+
metadataRow.frontmatter_start_offset != null &&
|
|
470
|
+
metadataRow.frontmatter_end_offset != null) {
|
|
471
|
+
result.frontmatter = {
|
|
472
|
+
position: {
|
|
473
|
+
start: {
|
|
474
|
+
line: metadataRow.frontmatter_start_line,
|
|
475
|
+
col: metadataRow.frontmatter_start_col,
|
|
476
|
+
offset: metadataRow.frontmatter_start_offset
|
|
477
|
+
},
|
|
478
|
+
end: {
|
|
479
|
+
line: metadataRow.frontmatter_end_line,
|
|
480
|
+
col: metadataRow.frontmatter_end_col,
|
|
481
|
+
offset: metadataRow.frontmatter_end_offset
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// Links
|
|
487
|
+
const linkRows = this.db.prepare('SELECT * FROM links_with_positions WHERE source_path = ?').all(filePath);
|
|
488
|
+
if (linkRows.length > 0) {
|
|
489
|
+
result.links = linkRows.map(row => ({
|
|
490
|
+
link: row.link_target || row.target_path || row.target_id || '',
|
|
491
|
+
original: row.original,
|
|
492
|
+
displayText: row.display_text ?? undefined,
|
|
493
|
+
position: {
|
|
494
|
+
start: {
|
|
495
|
+
line: row.start_line,
|
|
496
|
+
col: row.start_col,
|
|
497
|
+
offset: row.start_offset
|
|
498
|
+
},
|
|
499
|
+
end: {
|
|
500
|
+
line: row.end_line,
|
|
501
|
+
col: row.end_col,
|
|
502
|
+
offset: row.end_offset
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
// Tags
|
|
508
|
+
const tagRows = this.db.prepare('SELECT * FROM tags_with_positions WHERE file_path = ?').all(filePath);
|
|
509
|
+
if (tagRows.length > 0) {
|
|
510
|
+
result.tags = tagRows.map(row => ({
|
|
511
|
+
tag: row.tag,
|
|
512
|
+
position: {
|
|
513
|
+
start: {
|
|
514
|
+
line: row.start_line,
|
|
515
|
+
col: row.start_col,
|
|
516
|
+
offset: row.start_offset
|
|
517
|
+
},
|
|
518
|
+
end: {
|
|
519
|
+
line: row.end_line,
|
|
520
|
+
col: row.end_col,
|
|
521
|
+
offset: row.end_offset
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
// Headings
|
|
527
|
+
const headingRows = this.db.prepare('SELECT * FROM headings_with_positions WHERE file_path = ?').all(filePath);
|
|
528
|
+
if (headingRows.length > 0) {
|
|
529
|
+
result.headings = headingRows.map(row => ({
|
|
530
|
+
heading: row.heading,
|
|
531
|
+
level: row.level,
|
|
532
|
+
position: {
|
|
533
|
+
start: {
|
|
534
|
+
line: row.start_line,
|
|
535
|
+
col: row.start_col,
|
|
536
|
+
offset: row.start_offset
|
|
537
|
+
},
|
|
538
|
+
end: {
|
|
539
|
+
line: row.end_line,
|
|
540
|
+
col: row.end_col,
|
|
541
|
+
offset: row.end_offset
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
// Blocks
|
|
547
|
+
const blockRows = this.db.prepare('SELECT * FROM blocks_with_positions WHERE file_path = ?').all(filePath);
|
|
548
|
+
if (blockRows.length > 0) {
|
|
549
|
+
result.blocks = blockRows.map(row => ({
|
|
550
|
+
id: row.block_id,
|
|
551
|
+
position: {
|
|
552
|
+
start: {
|
|
553
|
+
line: row.start_line,
|
|
554
|
+
col: row.start_col,
|
|
555
|
+
offset: row.start_offset
|
|
556
|
+
},
|
|
557
|
+
end: {
|
|
558
|
+
line: row.end_line,
|
|
559
|
+
col: row.end_col,
|
|
560
|
+
offset: row.end_offset
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
// Embeds
|
|
566
|
+
const embedRows = this.db.prepare('SELECT * FROM embeds_with_positions WHERE file_path = ?').all(filePath);
|
|
567
|
+
if (embedRows.length > 0) {
|
|
568
|
+
result.embeds = embedRows.map(row => ({
|
|
569
|
+
link: row.target_path,
|
|
570
|
+
original: row.original,
|
|
571
|
+
displayText: row.display_text ?? undefined,
|
|
572
|
+
position: {
|
|
573
|
+
start: {
|
|
574
|
+
line: row.start_line,
|
|
575
|
+
col: row.start_col,
|
|
576
|
+
offset: row.start_offset
|
|
577
|
+
},
|
|
578
|
+
end: {
|
|
579
|
+
line: row.end_line,
|
|
580
|
+
col: row.end_col,
|
|
581
|
+
offset: row.end_offset
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
// Sections
|
|
587
|
+
const sectionRows = this.db.prepare('SELECT * FROM sections_with_positions WHERE file_path = ?').all(filePath);
|
|
588
|
+
if (sectionRows.length > 0) {
|
|
589
|
+
result.sections = sectionRows.map(row => ({
|
|
590
|
+
id: row.section_id,
|
|
591
|
+
type: row.type,
|
|
592
|
+
position: {
|
|
593
|
+
start: {
|
|
594
|
+
line: row.start_line,
|
|
595
|
+
col: row.start_col,
|
|
596
|
+
offset: row.start_offset
|
|
597
|
+
},
|
|
598
|
+
end: {
|
|
599
|
+
line: row.end_line,
|
|
600
|
+
col: row.end_col,
|
|
601
|
+
offset: row.end_offset
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
140
607
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
`;
|
|
148
|
-
const rows = this.db.prepare(sql).all();
|
|
149
|
-
return this.rowsToNotes(rows);
|
|
608
|
+
/**
|
|
609
|
+
* Get sections for a file (section-level query). Returns empty array if file has no metadata.
|
|
610
|
+
*/
|
|
611
|
+
getSectionsForFile(filePath) {
|
|
612
|
+
const meta = this.getContentMetadata(filePath);
|
|
613
|
+
return meta?.sections ?? [];
|
|
150
614
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
615
|
+
// Batch operations
|
|
616
|
+
upsertFilesBatch(files) {
|
|
617
|
+
if (files.length === 0)
|
|
618
|
+
return;
|
|
619
|
+
const stmt = this.db.prepare(`
|
|
620
|
+
INSERT INTO files (path, name, basename, extension, parent, ctime, mtime, size, content_hash)
|
|
621
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
622
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
623
|
+
name = excluded.name,
|
|
624
|
+
basename = excluded.basename,
|
|
625
|
+
extension = excluded.extension,
|
|
626
|
+
parent = excluded.parent,
|
|
627
|
+
ctime = excluded.ctime,
|
|
628
|
+
mtime = excluded.mtime,
|
|
629
|
+
size = excluded.size,
|
|
630
|
+
content_hash = excluded.content_hash
|
|
631
|
+
`);
|
|
632
|
+
const transaction = this.db.transaction(() => {
|
|
633
|
+
for (const { file, contentHash } of files) {
|
|
634
|
+
stmt.run(file.path, file.name, file.basename, file.extension, file.parent, file.stat.ctime, file.stat.mtime, file.stat.size, contentHash);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
transaction();
|
|
154
638
|
}
|
|
155
|
-
|
|
156
|
-
|
|
639
|
+
upsertContentMetadataBatch(items) {
|
|
640
|
+
if (items.length === 0)
|
|
641
|
+
return;
|
|
642
|
+
const transaction = this.db.transaction(() => {
|
|
643
|
+
for (const { filePath, metadata, contentHash } of items) {
|
|
644
|
+
this.doUpsertContentMetadata(filePath, metadata, contentHash);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
transaction();
|
|
157
648
|
}
|
|
158
649
|
getStats() {
|
|
159
|
-
const totalNotes = this.db.prepare('SELECT COUNT(*)
|
|
160
|
-
const totalLinks = this.db.prepare('SELECT COUNT(*)
|
|
161
|
-
const orphans = this.db.prepare(
|
|
650
|
+
const totalNotes = this.db.prepare('SELECT COUNT(*) AS count FROM files').get().count;
|
|
651
|
+
const totalLinks = this.db.prepare('SELECT COUNT(*) AS count FROM links_with_positions').get().count;
|
|
652
|
+
const orphans = this.db.prepare(`
|
|
653
|
+
SELECT COUNT(*) AS count FROM files f
|
|
654
|
+
WHERE NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.source_path = f.path)
|
|
655
|
+
AND NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.target_path = f.path)
|
|
656
|
+
`).get().count;
|
|
162
657
|
return { totalNotes, totalLinks, orphans };
|
|
163
658
|
}
|
|
659
|
+
/** Graph data from new structure (files + links_with_positions). */
|
|
164
660
|
getGraphData() {
|
|
165
|
-
const
|
|
166
|
-
|
|
661
|
+
const fileRows = this.db.prepare(`
|
|
662
|
+
SELECT f.path, f.basename,
|
|
663
|
+
(SELECT group_concat(DISTINCT tag) FROM tags_with_positions WHERE file_path = f.path) AS tags_str
|
|
664
|
+
FROM files f
|
|
665
|
+
`).all();
|
|
666
|
+
const edgeRows = this.db.prepare(`
|
|
667
|
+
SELECT DISTINCT source_path, target_path FROM links_with_positions WHERE target_path IS NOT NULL
|
|
668
|
+
`).all();
|
|
167
669
|
return {
|
|
168
|
-
nodes:
|
|
169
|
-
id: n.
|
|
170
|
-
title: n.
|
|
670
|
+
nodes: fileRows.map(n => ({
|
|
671
|
+
id: n.path,
|
|
672
|
+
title: n.basename,
|
|
171
673
|
path: n.path,
|
|
172
|
-
tags:
|
|
674
|
+
tags: n.tags_str ? n.tags_str.split(',') : []
|
|
173
675
|
})),
|
|
174
|
-
edges:
|
|
175
|
-
source: e.source_id,
|
|
176
|
-
target: e.target_id
|
|
177
|
-
}))
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
rowToNote(row) {
|
|
181
|
-
const links = this.db.prepare('SELECT target_id FROM links WHERE source_id = ?').all(row.id);
|
|
182
|
-
const backlinks = this.db.prepare('SELECT source_id FROM links WHERE target_id = ?').all(row.id);
|
|
183
|
-
return {
|
|
184
|
-
id: row.id,
|
|
185
|
-
path: row.path,
|
|
186
|
-
title: row.title,
|
|
187
|
-
content: row.content,
|
|
188
|
-
frontmatter: JSON.parse(row.frontmatter),
|
|
189
|
-
tags: JSON.parse(row.tags),
|
|
190
|
-
links: links.map((l) => l.target_id),
|
|
191
|
-
backlinks: backlinks.map((l) => l.source_id),
|
|
192
|
-
hash: row.hash,
|
|
193
|
-
createdAt: row.created_at,
|
|
194
|
-
modifiedAt: row.modified_at
|
|
676
|
+
edges: edgeRows.map(e => ({ source: e.source_path, target: e.target_path }))
|
|
195
677
|
};
|
|
196
678
|
}
|
|
197
|
-
getNotesWithLinksBatch(noteIds) {
|
|
198
|
-
if (noteIds.length === 0)
|
|
199
|
-
return new Map();
|
|
200
|
-
const placeholders = noteIds.map(() => '?').join(',');
|
|
201
|
-
// Single query to get all links for all notes
|
|
202
|
-
const linksSql = `
|
|
203
|
-
SELECT source_id, json_group_array(target_id) as targets
|
|
204
|
-
FROM links
|
|
205
|
-
WHERE source_id IN (${placeholders})
|
|
206
|
-
GROUP BY source_id
|
|
207
|
-
`;
|
|
208
|
-
const backlinksSql = `
|
|
209
|
-
SELECT target_id, json_group_array(source_id) as sources
|
|
210
|
-
FROM links
|
|
211
|
-
WHERE target_id IN (${placeholders})
|
|
212
|
-
GROUP BY target_id
|
|
213
|
-
`;
|
|
214
|
-
const result = new Map();
|
|
215
|
-
// Initialize with empty arrays
|
|
216
|
-
for (const id of noteIds) {
|
|
217
|
-
result.set(id, { links: [], backlinks: [] });
|
|
218
|
-
}
|
|
219
|
-
// Populate links
|
|
220
|
-
const linksRows = this.db.prepare(linksSql).all(...noteIds);
|
|
221
|
-
for (const row of linksRows) {
|
|
222
|
-
const targets = JSON.parse(row.targets);
|
|
223
|
-
result.get(row.source_id).links = targets;
|
|
224
|
-
}
|
|
225
|
-
// Populate backlinks
|
|
226
|
-
const backlinksRows = this.db.prepare(backlinksSql).all(...noteIds);
|
|
227
|
-
for (const row of backlinksRows) {
|
|
228
|
-
const sources = JSON.parse(row.sources);
|
|
229
|
-
result.get(row.target_id).backlinks = sources;
|
|
230
|
-
}
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
rowsToNotes(rows) {
|
|
234
|
-
if (rows.length === 0)
|
|
235
|
-
return [];
|
|
236
|
-
// Extract all note IDs
|
|
237
|
-
const noteIds = rows.map(row => row.id);
|
|
238
|
-
// Batch load all links and backlinks in 2 queries
|
|
239
|
-
const linkData = this.getNotesWithLinksBatch(noteIds);
|
|
240
|
-
// Map rows to notes using batched link data
|
|
241
|
-
return rows.map(row => {
|
|
242
|
-
const links = linkData.get(row.id)?.links || [];
|
|
243
|
-
const backlinks = linkData.get(row.id)?.backlinks || [];
|
|
244
|
-
return {
|
|
245
|
-
id: row.id,
|
|
246
|
-
path: row.path,
|
|
247
|
-
title: row.title,
|
|
248
|
-
content: row.content,
|
|
249
|
-
frontmatter: JSON.parse(row.frontmatter),
|
|
250
|
-
tags: JSON.parse(row.tags),
|
|
251
|
-
links,
|
|
252
|
-
backlinks,
|
|
253
|
-
hash: row.hash,
|
|
254
|
-
createdAt: row.created_at,
|
|
255
|
-
modifiedAt: row.modified_at,
|
|
256
|
-
};
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
679
|
}
|
|
260
680
|
exports.DatabaseManager = DatabaseManager;
|
|
261
681
|
//# sourceMappingURL=database.js.map
|