@dinoxx/dinox-cli 1.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/README.md +294 -0
- package/dist/auth/userInfo.d.ts +14 -0
- package/dist/auth/userInfo.js +115 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/cliTypes.d.ts +6 -0
- package/dist/cliTypes.js +1 -0
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +193 -0
- package/dist/commands/boxes/index.d.ts +2 -0
- package/dist/commands/boxes/index.js +107 -0
- package/dist/commands/boxes/repo.d.ts +21 -0
- package/dist/commands/boxes/repo.js +154 -0
- package/dist/commands/config/index.d.ts +2 -0
- package/dist/commands/config/index.js +67 -0
- package/dist/commands/info/index.d.ts +2 -0
- package/dist/commands/info/index.js +20 -0
- package/dist/commands/notes/index.d.ts +2 -0
- package/dist/commands/notes/index.js +271 -0
- package/dist/commands/notes/repo.d.ts +70 -0
- package/dist/commands/notes/repo.js +674 -0
- package/dist/commands/notes/searchTime.d.ts +9 -0
- package/dist/commands/notes/searchTime.js +85 -0
- package/dist/commands/prompt/index.d.ts +2 -0
- package/dist/commands/prompt/index.js +51 -0
- package/dist/commands/prompt/repo.d.ts +6 -0
- package/dist/commands/prompt/repo.js +18 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +68 -0
- package/dist/commands/tags/index.d.ts +2 -0
- package/dist/commands/tags/index.js +120 -0
- package/dist/commands/tags/repo.d.ts +14 -0
- package/dist/commands/tags/repo.js +247 -0
- package/dist/config/keys.d.ts +9 -0
- package/dist/config/keys.js +17 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +39 -0
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +56 -0
- package/dist/config/serviceEndpoints.d.ts +3 -0
- package/dist/config/serviceEndpoints.js +3 -0
- package/dist/config/store.d.ts +5 -0
- package/dist/config/store.js +87 -0
- package/dist/config/types.d.ts +51 -0
- package/dist/config/types.js +1 -0
- package/dist/dinox.d.ts +2 -0
- package/dist/dinox.js +50 -0
- package/dist/powersync/connector.d.ts +21 -0
- package/dist/powersync/connector.js +58 -0
- package/dist/powersync/runtime.d.ts +37 -0
- package/dist/powersync/runtime.js +107 -0
- package/dist/powersync/schema/content.d.ts +76 -0
- package/dist/powersync/schema/content.js +76 -0
- package/dist/powersync/schema/index.d.ts +371 -0
- package/dist/powersync/schema/index.js +35 -0
- package/dist/powersync/schema/local.d.ts +68 -0
- package/dist/powersync/schema/local.js +83 -0
- package/dist/powersync/schema/note.d.ts +34 -0
- package/dist/powersync/schema/note.js +34 -0
- package/dist/powersync/schema/notesExtras.d.ts +62 -0
- package/dist/powersync/schema/notesExtras.js +71 -0
- package/dist/powersync/schema/projects.d.ts +101 -0
- package/dist/powersync/schema/projects.js +101 -0
- package/dist/powersync/schema/tags.d.ts +37 -0
- package/dist/powersync/schema/tags.js +37 -0
- package/dist/powersync/tokenIndex.d.ts +17 -0
- package/dist/powersync/tokenIndex.js +202 -0
- package/dist/powersync/uploader.d.ts +7 -0
- package/dist/powersync/uploader.js +134 -0
- package/dist/utils/argValue.d.ts +1 -0
- package/dist/utils/argValue.js +17 -0
- package/dist/utils/errors.d.ts +10 -0
- package/dist/utils/errors.js +17 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.js +4 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +10 -0
- package/dist/utils/text.d.ts +1 -0
- package/dist/utils/text.js +35 -0
- package/dist/utils/time.d.ts +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/tiptapMarkdown.d.ts +6 -0
- package/dist/utils/tiptapMarkdown.js +149 -0
- package/dist/utils/tokenize.d.ts +1 -0
- package/dist/utils/tokenize.js +56 -0
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.js +21 -0
- package/package.json +63 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
2
|
+
import { nowIsoUtc } from '../../utils/time.js';
|
|
3
|
+
import { tokenizeWithJieba } from '../../utils/tokenize.js';
|
|
4
|
+
import { listBoxes } from '../boxes/repo.js';
|
|
5
|
+
import { listTags } from '../tags/repo.js';
|
|
6
|
+
export async function searchNotes(db, query, options) {
|
|
7
|
+
const normalizedQuery = query.trim();
|
|
8
|
+
const tagFilter = buildTagFilter(options.tagsExpression, 'n');
|
|
9
|
+
const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
|
|
10
|
+
let rows = [];
|
|
11
|
+
if (!options.includeDeleted) {
|
|
12
|
+
const ftsMatch = buildFtsMatchQuery(normalizedQuery);
|
|
13
|
+
if (ftsMatch) {
|
|
14
|
+
try {
|
|
15
|
+
rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter);
|
|
16
|
+
return enrichSearchRows(db, rows);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
const normalized = message.toLowerCase();
|
|
21
|
+
// FTS table may not be initialized yet, or query syntax may fail.
|
|
22
|
+
if (normalized.includes('no such table') ||
|
|
23
|
+
normalized.includes('fts5') ||
|
|
24
|
+
normalized.includes('malformed match') ||
|
|
25
|
+
normalized.includes('syntax error')) {
|
|
26
|
+
// Fallback to legacy LIKE search below.
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const whereClauses = [
|
|
35
|
+
options.includeDeleted ? '1 = 1' : '(n.is_del IS NULL OR n.is_del = 0)',
|
|
36
|
+
];
|
|
37
|
+
const params = [];
|
|
38
|
+
if (normalizedQuery.length > 0) {
|
|
39
|
+
const like = `%${normalizedQuery}%`;
|
|
40
|
+
whereClauses.push(`
|
|
41
|
+
(
|
|
42
|
+
(n.title IS NOT NULL AND n.title LIKE ?)
|
|
43
|
+
OR (n.content_text IS NOT NULL AND n.content_text LIKE ?)
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
params.push(like, like);
|
|
47
|
+
}
|
|
48
|
+
if (tagFilter) {
|
|
49
|
+
whereClauses.push(tagFilter.sql);
|
|
50
|
+
params.push(...tagFilter.params);
|
|
51
|
+
}
|
|
52
|
+
if (createdAtFilter) {
|
|
53
|
+
whereClauses.push(createdAtFilter.sql);
|
|
54
|
+
params.push(...createdAtFilter.params);
|
|
55
|
+
}
|
|
56
|
+
rows = await db.getAll(`
|
|
57
|
+
SELECT
|
|
58
|
+
n.id,
|
|
59
|
+
n.title,
|
|
60
|
+
n.summary,
|
|
61
|
+
n.content_text,
|
|
62
|
+
n.zettel_boxes,
|
|
63
|
+
n.updated_at,
|
|
64
|
+
n.is_del,
|
|
65
|
+
n.version
|
|
66
|
+
FROM c_note n
|
|
67
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
68
|
+
ORDER BY COALESCE(n.updated_at, n.created_at) DESC
|
|
69
|
+
`, params);
|
|
70
|
+
return enrichSearchRows(db, rows);
|
|
71
|
+
}
|
|
72
|
+
async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
|
|
73
|
+
const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
|
|
74
|
+
const params = [matchQuery];
|
|
75
|
+
if (tagFilter) {
|
|
76
|
+
whereClauses.push(tagFilter.sql);
|
|
77
|
+
params.push(...tagFilter.params);
|
|
78
|
+
}
|
|
79
|
+
if (createdAtFilter) {
|
|
80
|
+
whereClauses.push(createdAtFilter.sql);
|
|
81
|
+
params.push(...createdAtFilter.params);
|
|
82
|
+
}
|
|
83
|
+
return db.getAll(`
|
|
84
|
+
SELECT
|
|
85
|
+
n.id,
|
|
86
|
+
n.title,
|
|
87
|
+
n.summary,
|
|
88
|
+
n.content_text,
|
|
89
|
+
n.zettel_boxes,
|
|
90
|
+
n.updated_at,
|
|
91
|
+
n.is_del,
|
|
92
|
+
n.version
|
|
93
|
+
FROM note_local_fts f
|
|
94
|
+
JOIN c_note n ON n.id = f.note_id
|
|
95
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
96
|
+
ORDER BY bm25(note_local_fts, 8.0, 4.0, 1.0) ASC, COALESCE(n.updated_at, n.created_at) DESC
|
|
97
|
+
`, params);
|
|
98
|
+
}
|
|
99
|
+
async function enrichSearchRows(db, rows) {
|
|
100
|
+
const allBoxIds = Array.from(new Set(rows.flatMap((row) => parseZettelBoxIds(row.zettel_boxes))));
|
|
101
|
+
const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
|
|
102
|
+
return rows.map((row) => {
|
|
103
|
+
const summary = resolveSummary(row.summary, row.content_text);
|
|
104
|
+
const zettelBoxes = parseZettelBoxIds(row.zettel_boxes)
|
|
105
|
+
.map((id) => boxNameMap.get(id))
|
|
106
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
title: row.title?.trim() ?? '',
|
|
110
|
+
summary,
|
|
111
|
+
updated_at: row.updated_at,
|
|
112
|
+
is_del: row.is_del,
|
|
113
|
+
version: row.version,
|
|
114
|
+
zettel_boxes: zettelBoxes,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function resolveSummary(summary, contentText) {
|
|
119
|
+
const summaryValue = summary?.trim() ?? '';
|
|
120
|
+
if (summaryValue.length > 0) {
|
|
121
|
+
return summaryValue;
|
|
122
|
+
}
|
|
123
|
+
return contentText?.trim() ?? '';
|
|
124
|
+
}
|
|
125
|
+
function parseZettelBoxIds(raw) {
|
|
126
|
+
const value = raw?.trim();
|
|
127
|
+
if (!value) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(value);
|
|
132
|
+
if (!Array.isArray(parsed)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return Array.from(new Set(parsed
|
|
136
|
+
.map((entry) => String(entry).trim())
|
|
137
|
+
.filter((entry) => entry.length > 0)));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function loadZettelBoxNameMap(db, ids) {
|
|
144
|
+
const map = new Map();
|
|
145
|
+
if (ids.length === 0) {
|
|
146
|
+
return map;
|
|
147
|
+
}
|
|
148
|
+
const chunkSize = 500;
|
|
149
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
150
|
+
const batch = ids.slice(i, i + chunkSize);
|
|
151
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
152
|
+
const rows = await db.getAll(`
|
|
153
|
+
SELECT id, name
|
|
154
|
+
FROM c_zettel_box
|
|
155
|
+
WHERE (is_del IS NULL OR is_del = 0)
|
|
156
|
+
AND id IN (${placeholders})
|
|
157
|
+
`, batch);
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
const id = row.id?.trim() ?? '';
|
|
160
|
+
if (!id) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
map.set(id, row.name?.trim() ?? '');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return map;
|
|
167
|
+
}
|
|
168
|
+
function buildFtsMatchQuery(query) {
|
|
169
|
+
const tokens = tokenizeWithJieba(query, 32)
|
|
170
|
+
.map((token) => token.trim())
|
|
171
|
+
.filter((token) => token.length > 0);
|
|
172
|
+
if (tokens.length === 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const unique = Array.from(new Set(tokens));
|
|
176
|
+
return unique.map((token) => `"${escapeFtsToken(token)}"`).join(' AND ');
|
|
177
|
+
}
|
|
178
|
+
function escapeFtsToken(token) {
|
|
179
|
+
return token.replace(/"/g, '""');
|
|
180
|
+
}
|
|
181
|
+
function buildTagFilter(tagsExpression, noteAlias) {
|
|
182
|
+
const expression = tagsExpression?.trim() ?? '';
|
|
183
|
+
if (!expression) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
let ast;
|
|
187
|
+
try {
|
|
188
|
+
ast = parseTagExpression(expression);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
throw new DinoxError(`Invalid --tags expression: ${message}`);
|
|
193
|
+
}
|
|
194
|
+
const params = [];
|
|
195
|
+
const sql = compileTagExpression(ast, noteAlias, params);
|
|
196
|
+
return { sql: `(${sql})`, params };
|
|
197
|
+
}
|
|
198
|
+
function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
|
|
199
|
+
const clauses = [];
|
|
200
|
+
const params = [];
|
|
201
|
+
if (createdAtFrom) {
|
|
202
|
+
clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at >= ?)`);
|
|
203
|
+
params.push(createdAtFrom);
|
|
204
|
+
}
|
|
205
|
+
if (createdAtTo) {
|
|
206
|
+
clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at <= ?)`);
|
|
207
|
+
params.push(createdAtTo);
|
|
208
|
+
}
|
|
209
|
+
if (clauses.length === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
sql: `(${clauses.join(' AND ')})`,
|
|
214
|
+
params,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function parseTagExpression(expression) {
|
|
218
|
+
const tokens = tokenizeTagExpression(expression);
|
|
219
|
+
if (tokens.length === 0) {
|
|
220
|
+
throw new Error('Expression is empty');
|
|
221
|
+
}
|
|
222
|
+
let index = 0;
|
|
223
|
+
const peek = () => tokens[index] ?? null;
|
|
224
|
+
const consume = (expected) => {
|
|
225
|
+
const token = peek();
|
|
226
|
+
if (!token) {
|
|
227
|
+
throw new Error('Unexpected end of expression');
|
|
228
|
+
}
|
|
229
|
+
if (expected && token.type !== expected) {
|
|
230
|
+
throw new Error(`Expected ${expected} at position ${token.position + 1}`);
|
|
231
|
+
}
|
|
232
|
+
index += 1;
|
|
233
|
+
return token;
|
|
234
|
+
};
|
|
235
|
+
const parsePrimary = () => {
|
|
236
|
+
const token = peek();
|
|
237
|
+
if (!token) {
|
|
238
|
+
throw new Error('Unexpected end of expression');
|
|
239
|
+
}
|
|
240
|
+
if (token.type === 'TERM') {
|
|
241
|
+
consume('TERM');
|
|
242
|
+
const value = normalizeTagValue(token.value);
|
|
243
|
+
if (!value) {
|
|
244
|
+
throw new Error(`Empty tag at position ${token.position + 1}`);
|
|
245
|
+
}
|
|
246
|
+
return { kind: 'tag', value };
|
|
247
|
+
}
|
|
248
|
+
if (token.type === 'LPAREN') {
|
|
249
|
+
consume('LPAREN');
|
|
250
|
+
const node = parseOr();
|
|
251
|
+
const close = peek();
|
|
252
|
+
if (!close || close.type !== 'RPAREN') {
|
|
253
|
+
throw new Error(`Missing ')' for '(' at position ${token.position + 1}`);
|
|
254
|
+
}
|
|
255
|
+
consume('RPAREN');
|
|
256
|
+
return node;
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Unexpected token '${token.value}' at position ${token.position + 1}`);
|
|
259
|
+
};
|
|
260
|
+
const parseUnary = () => {
|
|
261
|
+
const token = peek();
|
|
262
|
+
if (token?.type === 'NOT') {
|
|
263
|
+
consume('NOT');
|
|
264
|
+
return { kind: 'not', child: parseUnary() };
|
|
265
|
+
}
|
|
266
|
+
return parsePrimary();
|
|
267
|
+
};
|
|
268
|
+
const parseAnd = () => {
|
|
269
|
+
let node = parseUnary();
|
|
270
|
+
while (peek()?.type === 'AND') {
|
|
271
|
+
consume('AND');
|
|
272
|
+
node = { kind: 'and', left: node, right: parseUnary() };
|
|
273
|
+
}
|
|
274
|
+
return node;
|
|
275
|
+
};
|
|
276
|
+
const parseOr = () => {
|
|
277
|
+
let node = parseAnd();
|
|
278
|
+
while (peek()?.type === 'OR') {
|
|
279
|
+
consume('OR');
|
|
280
|
+
node = { kind: 'or', left: node, right: parseAnd() };
|
|
281
|
+
}
|
|
282
|
+
return node;
|
|
283
|
+
};
|
|
284
|
+
const ast = parseOr();
|
|
285
|
+
const extra = peek();
|
|
286
|
+
if (extra) {
|
|
287
|
+
throw new Error(`Unexpected token '${extra.value}' at position ${extra.position + 1}`);
|
|
288
|
+
}
|
|
289
|
+
return ast;
|
|
290
|
+
}
|
|
291
|
+
function tokenizeTagExpression(expression) {
|
|
292
|
+
const tokens = [];
|
|
293
|
+
let i = 0;
|
|
294
|
+
while (i < expression.length) {
|
|
295
|
+
const ch = expression[i];
|
|
296
|
+
if (/\s/.test(ch)) {
|
|
297
|
+
i += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (ch === '(') {
|
|
301
|
+
tokens.push({ type: 'LPAREN', value: ch, position: i });
|
|
302
|
+
i += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ch === ')') {
|
|
306
|
+
tokens.push({ type: 'RPAREN', value: ch, position: i });
|
|
307
|
+
i += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === '"' || ch === "'") {
|
|
311
|
+
const quoted = readQuotedToken(expression, i);
|
|
312
|
+
tokens.push({ type: 'TERM', value: quoted.value, position: i });
|
|
313
|
+
i = quoted.next;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
let j = i;
|
|
317
|
+
while (j < expression.length) {
|
|
318
|
+
const next = expression[j];
|
|
319
|
+
if (/\s/.test(next) || next === '(' || next === ')') {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
j += 1;
|
|
323
|
+
}
|
|
324
|
+
const raw = expression.slice(i, j);
|
|
325
|
+
const upper = raw.toUpperCase();
|
|
326
|
+
if (upper === 'AND' || upper === 'OR' || upper === 'NOT') {
|
|
327
|
+
tokens.push({ type: upper, value: raw, position: i });
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
tokens.push({ type: 'TERM', value: raw, position: i });
|
|
331
|
+
}
|
|
332
|
+
i = j;
|
|
333
|
+
}
|
|
334
|
+
return tokens;
|
|
335
|
+
}
|
|
336
|
+
function readQuotedToken(expression, start) {
|
|
337
|
+
const quote = expression[start];
|
|
338
|
+
let i = start + 1;
|
|
339
|
+
let value = '';
|
|
340
|
+
while (i < expression.length) {
|
|
341
|
+
const ch = expression[i];
|
|
342
|
+
if (ch === '\\') {
|
|
343
|
+
if (i + 1 >= expression.length) {
|
|
344
|
+
throw new Error(`Invalid escape at position ${i + 1}`);
|
|
345
|
+
}
|
|
346
|
+
value += expression[i + 1];
|
|
347
|
+
i += 2;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (ch === quote) {
|
|
351
|
+
return { value, next: i + 1 };
|
|
352
|
+
}
|
|
353
|
+
value += ch;
|
|
354
|
+
i += 1;
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`Unclosed quote at position ${start + 1}`);
|
|
357
|
+
}
|
|
358
|
+
function compileTagExpression(node, noteAlias, params) {
|
|
359
|
+
if (node.kind === 'tag') {
|
|
360
|
+
params.push(node.value);
|
|
361
|
+
return `
|
|
362
|
+
EXISTS (
|
|
363
|
+
SELECT 1
|
|
364
|
+
FROM json_each(
|
|
365
|
+
CASE
|
|
366
|
+
WHEN json_valid(COALESCE(${noteAlias}.tags, '')) THEN ${noteAlias}.tags
|
|
367
|
+
ELSE '[]'
|
|
368
|
+
END
|
|
369
|
+
) tag_item
|
|
370
|
+
WHERE LOWER(TRIM(CAST(tag_item.value AS TEXT))) = ?
|
|
371
|
+
)
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
if (node.kind === 'not') {
|
|
375
|
+
return `NOT (${compileTagExpression(node.child, noteAlias, params)})`;
|
|
376
|
+
}
|
|
377
|
+
if (node.kind === 'and') {
|
|
378
|
+
return `(${compileTagExpression(node.left, noteAlias, params)}) AND (${compileTagExpression(node.right, noteAlias, params)})`;
|
|
379
|
+
}
|
|
380
|
+
return `(${compileTagExpression(node.left, noteAlias, params)}) OR (${compileTagExpression(node.right, noteAlias, params)})`;
|
|
381
|
+
}
|
|
382
|
+
function normalizeTagValue(value) {
|
|
383
|
+
return value.trim().toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
export async function getNote(db, id) {
|
|
386
|
+
return db.getOptional(`
|
|
387
|
+
SELECT
|
|
388
|
+
id,
|
|
389
|
+
title,
|
|
390
|
+
content_md,
|
|
391
|
+
content_json,
|
|
392
|
+
content_text,
|
|
393
|
+
created_at,
|
|
394
|
+
updated_at,
|
|
395
|
+
is_del,
|
|
396
|
+
version
|
|
397
|
+
FROM c_note
|
|
398
|
+
WHERE id = ?
|
|
399
|
+
`, [id]);
|
|
400
|
+
}
|
|
401
|
+
export async function getNoteDetail(db, id) {
|
|
402
|
+
const note = await db.getOptional(`
|
|
403
|
+
SELECT
|
|
404
|
+
id,
|
|
405
|
+
content,
|
|
406
|
+
audios,
|
|
407
|
+
images,
|
|
408
|
+
created_at,
|
|
409
|
+
updated_at,
|
|
410
|
+
title,
|
|
411
|
+
type,
|
|
412
|
+
source,
|
|
413
|
+
resource_id,
|
|
414
|
+
tags,
|
|
415
|
+
is_del,
|
|
416
|
+
content_md,
|
|
417
|
+
format_type,
|
|
418
|
+
zettel_boxes AS zettel_boxes_raw
|
|
419
|
+
FROM c_note
|
|
420
|
+
WHERE id = ?
|
|
421
|
+
`, [id]);
|
|
422
|
+
if (!note) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
426
|
+
const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
|
|
427
|
+
const zettelBoxes = boxIds
|
|
428
|
+
.map((boxId) => boxNameMap.get(boxId))
|
|
429
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
430
|
+
return {
|
|
431
|
+
id: note.id,
|
|
432
|
+
content: note.content,
|
|
433
|
+
audios: note.audios,
|
|
434
|
+
images: note.images,
|
|
435
|
+
created_at: note.created_at,
|
|
436
|
+
updated_at: note.updated_at,
|
|
437
|
+
title: note.title,
|
|
438
|
+
type: note.type,
|
|
439
|
+
source: note.source,
|
|
440
|
+
resource_id: note.resource_id,
|
|
441
|
+
tags: note.tags,
|
|
442
|
+
is_del: note.is_del,
|
|
443
|
+
content_md: note.content_md,
|
|
444
|
+
format_type: note.format_type,
|
|
445
|
+
zettel_boxes: zettelBoxes,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
export async function createNote(db, input) {
|
|
449
|
+
const userId = input.userId.trim();
|
|
450
|
+
if (!userId) {
|
|
451
|
+
throw new DinoxError('userId is required to create a note');
|
|
452
|
+
}
|
|
453
|
+
const now = nowIsoUtc();
|
|
454
|
+
const contentJson = JSON.stringify(input.contentJson);
|
|
455
|
+
const tags = JSON.stringify(input.tags);
|
|
456
|
+
const zettelBoxes = JSON.stringify(input.zettelBoxIds);
|
|
457
|
+
const columns = await db.getAll('PRAGMA table_info(c_note)', []);
|
|
458
|
+
const columnNames = new Set(columns
|
|
459
|
+
.map((row) => row.name?.trim().toLowerCase() ?? '')
|
|
460
|
+
.filter((name) => name.length > 0));
|
|
461
|
+
const userColumns = [];
|
|
462
|
+
if (columnNames.has('user_id')) {
|
|
463
|
+
userColumns.push('user_id');
|
|
464
|
+
}
|
|
465
|
+
if (columnNames.has('user_no')) {
|
|
466
|
+
userColumns.push('user_no');
|
|
467
|
+
}
|
|
468
|
+
if (userColumns.length === 0) {
|
|
469
|
+
throw new DinoxError('c_note table is missing both user_id and user_no columns');
|
|
470
|
+
}
|
|
471
|
+
if (!columnNames.has('type')) {
|
|
472
|
+
throw new DinoxError('c_note table is missing type column');
|
|
473
|
+
}
|
|
474
|
+
const requiredColumns = [
|
|
475
|
+
'id',
|
|
476
|
+
'title',
|
|
477
|
+
'type',
|
|
478
|
+
'content_md',
|
|
479
|
+
'content_json',
|
|
480
|
+
'content_text',
|
|
481
|
+
'tags',
|
|
482
|
+
'zettel_boxes',
|
|
483
|
+
'created_at',
|
|
484
|
+
'updated_at',
|
|
485
|
+
'is_del',
|
|
486
|
+
'version',
|
|
487
|
+
];
|
|
488
|
+
const missingRequired = requiredColumns.filter((name) => !columnNames.has(name));
|
|
489
|
+
if (missingRequired.length > 0) {
|
|
490
|
+
throw new DinoxError(`c_note table is missing required columns: ${missingRequired.join(', ')}`);
|
|
491
|
+
}
|
|
492
|
+
const fullInsertEntries = [
|
|
493
|
+
['id', input.id],
|
|
494
|
+
['title', input.title],
|
|
495
|
+
['type', input.noteType],
|
|
496
|
+
['user_no', userId],
|
|
497
|
+
['user_id', userId],
|
|
498
|
+
['created_at', now],
|
|
499
|
+
['updated_at', now],
|
|
500
|
+
['is_del', 0],
|
|
501
|
+
['status', 0],
|
|
502
|
+
['version', 1],
|
|
503
|
+
['content', ''],
|
|
504
|
+
['content_json', contentJson],
|
|
505
|
+
['content_text', input.contentText],
|
|
506
|
+
['content_md', input.contentMd],
|
|
507
|
+
['summary', ''],
|
|
508
|
+
['tags', tags],
|
|
509
|
+
['zettel_boxes', zettelBoxes],
|
|
510
|
+
['images', '[]'],
|
|
511
|
+
['files', '[]'],
|
|
512
|
+
['audios', '[]'],
|
|
513
|
+
['audio_detail', '{}'],
|
|
514
|
+
['image_detail', '{}'],
|
|
515
|
+
['source', ''],
|
|
516
|
+
['resource_id', null],
|
|
517
|
+
['note_ids', '[]'],
|
|
518
|
+
['extra_data', '{}'],
|
|
519
|
+
['format_type', ''],
|
|
520
|
+
['comments', '[]'],
|
|
521
|
+
['is_top', 0],
|
|
522
|
+
['pinned_at', '1997-01-01T00:00:00.000Z'],
|
|
523
|
+
['chat_context', '{}'],
|
|
524
|
+
['bid_links', '[]'],
|
|
525
|
+
];
|
|
526
|
+
const insertEntries = fullInsertEntries.filter(([column]) => columnNames.has(column));
|
|
527
|
+
const insertColumns = insertEntries.map(([column]) => column);
|
|
528
|
+
const placeholders = insertEntries.map(() => '?').join(', ');
|
|
529
|
+
const params = insertEntries.map(([, value]) => value);
|
|
530
|
+
await db.execute(`
|
|
531
|
+
INSERT INTO c_note (
|
|
532
|
+
${insertColumns.join(', ')}
|
|
533
|
+
) VALUES (${placeholders})
|
|
534
|
+
`, params);
|
|
535
|
+
return { id: input.id };
|
|
536
|
+
}
|
|
537
|
+
function normalizeLookupKey(value) {
|
|
538
|
+
return value.trim().toLowerCase();
|
|
539
|
+
}
|
|
540
|
+
export async function validateTagsForCreate(db, requestedTags) {
|
|
541
|
+
if (requestedTags.length === 0) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const tags = await listTags(db);
|
|
545
|
+
const existingTagMap = new Map();
|
|
546
|
+
for (const tag of tags) {
|
|
547
|
+
const key = normalizeLookupKey(tag);
|
|
548
|
+
if (!key || existingTagMap.has(key)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
existingTagMap.set(key, tag);
|
|
552
|
+
}
|
|
553
|
+
const resolved = [];
|
|
554
|
+
const seen = new Set();
|
|
555
|
+
const missing = [];
|
|
556
|
+
for (const requested of requestedTags) {
|
|
557
|
+
const key = normalizeLookupKey(requested);
|
|
558
|
+
if (!key || seen.has(key)) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
seen.add(key);
|
|
562
|
+
const matched = existingTagMap.get(key);
|
|
563
|
+
if (!matched) {
|
|
564
|
+
missing.push(requested);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
resolved.push(matched);
|
|
568
|
+
}
|
|
569
|
+
if (missing.length > 0) {
|
|
570
|
+
throw new DinoxError(`Unknown tags: ${missing.join(', ')}`, {
|
|
571
|
+
details: {
|
|
572
|
+
type: 'missing_tags',
|
|
573
|
+
missing,
|
|
574
|
+
knownCount: tags.length,
|
|
575
|
+
knownSample: tags.slice(0, 20),
|
|
576
|
+
question: 'Do you want to add these missing tags and retry `dino note create`?',
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return resolved;
|
|
581
|
+
}
|
|
582
|
+
export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
|
|
583
|
+
if (requestedNames.length === 0) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
const boxes = await listBoxes(db);
|
|
587
|
+
const availableNames = Array.from(new Set(boxes.map((box) => box.name.trim()).filter(Boolean)));
|
|
588
|
+
const boxByName = new Map();
|
|
589
|
+
for (const box of boxes) {
|
|
590
|
+
const name = box.name.trim();
|
|
591
|
+
const key = normalizeLookupKey(name);
|
|
592
|
+
if (!key) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const current = boxByName.get(key);
|
|
596
|
+
if (!current) {
|
|
597
|
+
boxByName.set(key, {
|
|
598
|
+
name,
|
|
599
|
+
ids: [box.id],
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (!current.ids.includes(box.id)) {
|
|
604
|
+
current.ids.push(box.id);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const resolvedIds = [];
|
|
608
|
+
const seenIds = new Set();
|
|
609
|
+
const missing = [];
|
|
610
|
+
const ambiguous = [];
|
|
611
|
+
const seenAmbiguous = new Set();
|
|
612
|
+
for (const requested of requestedNames) {
|
|
613
|
+
const key = normalizeLookupKey(requested);
|
|
614
|
+
if (!key) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const lookup = boxByName.get(key);
|
|
618
|
+
if (!lookup) {
|
|
619
|
+
missing.push(requested);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (lookup.ids.length > 1) {
|
|
623
|
+
const marker = `${requested.toLowerCase()}::${lookup.ids.join(',')}`;
|
|
624
|
+
if (!seenAmbiguous.has(marker)) {
|
|
625
|
+
seenAmbiguous.add(marker);
|
|
626
|
+
ambiguous.push({
|
|
627
|
+
name: requested,
|
|
628
|
+
ids: lookup.ids,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const id = lookup.ids[0];
|
|
634
|
+
if (!seenIds.has(id)) {
|
|
635
|
+
seenIds.add(id);
|
|
636
|
+
resolvedIds.push(id);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (ambiguous.length > 0) {
|
|
640
|
+
throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.ids.join(', ')}]`).join('; ')}`, {
|
|
641
|
+
details: {
|
|
642
|
+
type: 'ambiguous_zettel_boxes',
|
|
643
|
+
ambiguous,
|
|
644
|
+
availableNamesSample: availableNames.slice(0, 20),
|
|
645
|
+
question: 'Do you want to specify unique box names (or IDs) and retry `dino note create`?',
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (missing.length > 0) {
|
|
650
|
+
throw new DinoxError(`Unknown zettel box names: ${missing.join(', ')}`, {
|
|
651
|
+
details: {
|
|
652
|
+
type: 'missing_zettel_boxes',
|
|
653
|
+
missing,
|
|
654
|
+
availableCount: availableNames.length,
|
|
655
|
+
availableNamesSample: availableNames.slice(0, 20),
|
|
656
|
+
question: 'Do you want to add these missing boxes and retry `dino note create`?',
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return resolvedIds;
|
|
661
|
+
}
|
|
662
|
+
export async function softDeleteNote(db, id) {
|
|
663
|
+
const existing = await db.getOptional('SELECT id FROM c_note WHERE id = ?', [id]);
|
|
664
|
+
if (!existing) {
|
|
665
|
+
throw new DinoxError(`Note not found: ${id}`);
|
|
666
|
+
}
|
|
667
|
+
await db.execute(`
|
|
668
|
+
UPDATE c_note
|
|
669
|
+
SET is_del = 1,
|
|
670
|
+
updated_at = ?,
|
|
671
|
+
version = COALESCE(version, 0) + 1
|
|
672
|
+
WHERE id = ?
|
|
673
|
+
`, [nowIsoUtc(), id]);
|
|
674
|
+
}
|