@dinoxx/dinox-cli 1.0.6 → 1.0.7
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 +28 -4
- package/dist/commands/notes/index.js +108 -7
- package/dist/commands/notes/repo.d.ts +18 -13
- package/dist/commands/notes/repo.js +227 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -342,17 +342,23 @@ dino box add "你的卡片盒"
|
|
|
342
342
|
|
|
343
343
|
| 参数 | 必填 | 说明 |
|
|
344
344
|
| --- | --- | --- |
|
|
345
|
-
|
|
|
345
|
+
| `[id]` | 否 | 单个笔记 ID(可与 `--ids` 组合,去重后批量更新)。 |
|
|
346
346
|
|
|
347
347
|
支持全局参数:`--json`、`--offline`、`--sync-timeout`
|
|
348
348
|
|
|
349
|
-
### `dino note detail
|
|
349
|
+
### `dino note detail [id]`
|
|
350
350
|
|
|
351
|
-
按 ID
|
|
351
|
+
按 ID 获取笔记完整详情,支持批量读取。
|
|
352
352
|
|
|
353
353
|
| 参数 | 必填 | 说明 |
|
|
354
354
|
| --- | --- | --- |
|
|
355
|
-
|
|
|
355
|
+
| `[id]` | 否 | 单个笔记 ID(可与 `--ids` 组合,去重后批量读取)。 |
|
|
356
|
+
|
|
357
|
+
| 选项 | 必填 | 说明 |
|
|
358
|
+
| --- | --- | --- |
|
|
359
|
+
| `--ids <string\|@file>` | 否 | 笔记 ID 列表(JSON 数组或逗号/换行分隔)。 |
|
|
360
|
+
|
|
361
|
+
说明:`[id]` 与 `--ids` 至少提供一个。
|
|
356
362
|
|
|
357
363
|
支持全局参数:`--offline`、`--sync-timeout`
|
|
358
364
|
|
|
@@ -370,6 +376,24 @@ dino box add "你的卡片盒"
|
|
|
370
376
|
|
|
371
377
|
支持全局参数:`--json`、`--offline`、`--sync-timeout`
|
|
372
378
|
|
|
379
|
+
### `dino note update [id] [options]`
|
|
380
|
+
|
|
381
|
+
按 ID 更新笔记标签/卡片盒(全量替换)。
|
|
382
|
+
|
|
383
|
+
| 参数 | 必填 | 说明 |
|
|
384
|
+
| --- | --- | --- |
|
|
385
|
+
| `<id>` | 是 | 笔记 ID。 |
|
|
386
|
+
|
|
387
|
+
| 选项 | 必填 | 说明 |
|
|
388
|
+
| --- | --- | --- |
|
|
389
|
+
| `--ids <string\|@file>` | 否 | 批量更新的笔记 ID 列表(JSON 数组或逗号/换行分隔)。 |
|
|
390
|
+
| `--tags <string\|@file>` | 否 | 标签列表(JSON 数组或逗号/换行分隔)。传 `[]` 可清空标签。 |
|
|
391
|
+
| `--boxes <string\|@file>` | 否 | 卡片盒名称列表(JSON 数组或逗号/换行分隔)。传 `[]` 可清空卡片盒。 |
|
|
392
|
+
|
|
393
|
+
说明:`<id>` 与 `--ids` 至少提供一个;`--tags` 与 `--boxes` 至少提供一个。
|
|
394
|
+
|
|
395
|
+
支持全局参数:`--json`、`--offline`、`--sync-timeout`
|
|
396
|
+
|
|
373
397
|
### `dino note delete <id>`
|
|
374
398
|
|
|
375
399
|
软删除笔记(`is_del=1`)。
|
|
@@ -8,7 +8,7 @@ import { printYaml } from '../../utils/output.js';
|
|
|
8
8
|
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
9
9
|
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
10
10
|
import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
|
|
11
|
-
import { createNote, getNote,
|
|
11
|
+
import { createNote, getNote, getNoteDetails, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
|
|
12
12
|
import { parseCreatedAtRange } from './searchTime.js';
|
|
13
13
|
function parseSyncTimeoutMs(value) {
|
|
14
14
|
if (typeof value !== 'string') {
|
|
@@ -61,6 +61,23 @@ async function parseStringListInput(value, optionName) {
|
|
|
61
61
|
const raw = await readStringOrFile(value);
|
|
62
62
|
return parseListInput(raw, optionName);
|
|
63
63
|
}
|
|
64
|
+
async function parseTargetNoteIds(idArg, idsOption, commandName) {
|
|
65
|
+
const merged = [];
|
|
66
|
+
const idFromArg = typeof idArg === 'string' ? idArg.trim() : '';
|
|
67
|
+
if (idFromArg) {
|
|
68
|
+
merged.push(idFromArg);
|
|
69
|
+
}
|
|
70
|
+
if (typeof idsOption === 'string') {
|
|
71
|
+
merged.push(...(await parseStringListInput(idsOption, '--ids')));
|
|
72
|
+
}
|
|
73
|
+
const ids = Array.from(new Set(merged
|
|
74
|
+
.map((id) => id.trim())
|
|
75
|
+
.filter((id) => id.length > 0)));
|
|
76
|
+
if (ids.length === 0) {
|
|
77
|
+
throw new DinoxError(`Provide <id> or --ids for ${commandName}`);
|
|
78
|
+
}
|
|
79
|
+
return ids;
|
|
80
|
+
}
|
|
64
81
|
async function syncBeforeNoteCommand(db, offline, timeoutMs) {
|
|
65
82
|
if (offline) {
|
|
66
83
|
return;
|
|
@@ -154,8 +171,9 @@ export function registerNoteCommands(program) {
|
|
|
154
171
|
note
|
|
155
172
|
.command('detail')
|
|
156
173
|
.description('Get full note details by id')
|
|
157
|
-
.argument('
|
|
158
|
-
.
|
|
174
|
+
.argument('[id]', 'note id')
|
|
175
|
+
.option('--ids <string|@file>', 'note ids (JSON array or comma/newline-separated) for batch read')
|
|
176
|
+
.action(async (id, options, command) => {
|
|
159
177
|
const globals = command.optsWithGlobals?.() ?? {};
|
|
160
178
|
const offline = Boolean(globals.offline);
|
|
161
179
|
const config = resolveConfig(await loadConfig());
|
|
@@ -163,11 +181,22 @@ export function registerNoteCommands(program) {
|
|
|
163
181
|
const { db } = await connectPowerSync({ config, offline, timeoutMs });
|
|
164
182
|
try {
|
|
165
183
|
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
const ids = await parseTargetNoteIds(id, options.ids, 'detail');
|
|
185
|
+
const notes = await getNoteDetails(db, ids);
|
|
186
|
+
const foundIds = new Set(notes.map((note) => note.id));
|
|
187
|
+
const missing = ids.filter((noteId) => !foundIds.has(noteId));
|
|
188
|
+
if (missing.length > 0) {
|
|
189
|
+
if (ids.length === 1) {
|
|
190
|
+
throw new DinoxError(`Note not found: ${missing[0]}`);
|
|
191
|
+
}
|
|
192
|
+
throw new DinoxError(`Notes not found: ${missing.join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
if (notes.length === 1) {
|
|
195
|
+
printYaml(notes[0]);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
printYaml(notes);
|
|
169
199
|
}
|
|
170
|
-
printYaml(note);
|
|
171
200
|
}
|
|
172
201
|
finally {
|
|
173
202
|
await db.close().catch(() => undefined);
|
|
@@ -276,4 +305,76 @@ export function registerNoteCommands(program) {
|
|
|
276
305
|
await db.close().catch(() => undefined);
|
|
277
306
|
}
|
|
278
307
|
});
|
|
308
|
+
note
|
|
309
|
+
.command('update')
|
|
310
|
+
.description('Update note tags/boxes by id')
|
|
311
|
+
.argument('[id]', 'note id')
|
|
312
|
+
.option('--ids <string|@file>', 'note ids (JSON array or comma/newline-separated) for batch update')
|
|
313
|
+
.option('--tags <string|@file>', 'tag list (JSON array or comma/newline-separated)')
|
|
314
|
+
.option('--boxes <string|@file>', 'zettel box names (JSON array or comma/newline-separated)')
|
|
315
|
+
.action(async (id, options, command) => {
|
|
316
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
317
|
+
const offline = Boolean(globals.offline);
|
|
318
|
+
const jsonOutput = Boolean(globals.json);
|
|
319
|
+
const config = resolveConfig(await loadConfig());
|
|
320
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
321
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
322
|
+
try {
|
|
323
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
324
|
+
const ids = await parseTargetNoteIds(id, options.ids, 'update');
|
|
325
|
+
const hasTags = typeof options.tags === 'string';
|
|
326
|
+
const hasBoxes = typeof options.boxes === 'string';
|
|
327
|
+
if (!hasTags && !hasBoxes) {
|
|
328
|
+
throw new DinoxError('At least one option is required: --tags or --boxes');
|
|
329
|
+
}
|
|
330
|
+
const tags = hasTags
|
|
331
|
+
? await validateTagsForUpdate(db, await parseStringListInput(options.tags, '--tags'))
|
|
332
|
+
: undefined;
|
|
333
|
+
const zettelBoxIds = hasBoxes
|
|
334
|
+
? await resolveZettelBoxIdsForUpdate(db, await parseStringListInput(options.boxes, '--boxes'))
|
|
335
|
+
: undefined;
|
|
336
|
+
await assertNotesUpdatable(db, ids);
|
|
337
|
+
const results = [];
|
|
338
|
+
for (const targetId of ids) {
|
|
339
|
+
const result = await updateNote(db, { id: targetId, tags, zettelBoxIds });
|
|
340
|
+
results.push(result);
|
|
341
|
+
}
|
|
342
|
+
const updatedCount = results.filter((item) => item.updated).length;
|
|
343
|
+
if (!offline && !config.powersync.uploadBaseUrl) {
|
|
344
|
+
console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
|
|
345
|
+
}
|
|
346
|
+
if (!offline) {
|
|
347
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
|
|
348
|
+
}
|
|
349
|
+
const payload = {
|
|
350
|
+
ok: true,
|
|
351
|
+
total: results.length,
|
|
352
|
+
updatedCount,
|
|
353
|
+
results: results.map((item) => ({
|
|
354
|
+
id: item.id,
|
|
355
|
+
updated: item.updated,
|
|
356
|
+
tags: item.tags,
|
|
357
|
+
boxes: item.zettelBoxIds,
|
|
358
|
+
})),
|
|
359
|
+
stale: offline ? true : stale,
|
|
360
|
+
};
|
|
361
|
+
if (jsonOutput) {
|
|
362
|
+
printYaml(payload);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
if (results.length === 1) {
|
|
366
|
+
console.log(results[0].updated ? 'OK' : 'No changes');
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log(`Updated ${updatedCount}/${results.length} notes`);
|
|
370
|
+
}
|
|
371
|
+
if (stale && !offline) {
|
|
372
|
+
console.log('warning: sync timed out; local cache may be stale');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await db.close().catch(() => undefined);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
279
380
|
}
|
|
@@ -25,33 +25,30 @@ export type SearchedNote = {
|
|
|
25
25
|
id: string;
|
|
26
26
|
title: string;
|
|
27
27
|
summary: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
version: number | null;
|
|
28
|
+
tags: string[];
|
|
29
|
+
created_at: string | null;
|
|
31
30
|
zettel_boxes: string[];
|
|
32
31
|
};
|
|
33
|
-
type
|
|
32
|
+
export type NoteDetail = {
|
|
34
33
|
id: string;
|
|
35
|
-
content: string | null;
|
|
36
|
-
audios: string | null;
|
|
37
|
-
images: string | null;
|
|
38
34
|
created_at: string | null;
|
|
39
|
-
updated_at: string | null;
|
|
40
35
|
title: string | null;
|
|
41
36
|
type: string | null;
|
|
42
37
|
source: string | null;
|
|
43
|
-
resource_id: string | null;
|
|
44
38
|
tags: string | null;
|
|
45
39
|
is_del: number | null;
|
|
46
40
|
content_md: string | null;
|
|
47
|
-
format_type: string | null;
|
|
48
|
-
zettel_boxes_raw: string | null;
|
|
49
|
-
};
|
|
50
|
-
export type NoteDetail = Omit<NoteDetailRow, 'zettel_boxes_raw'> & {
|
|
51
41
|
zettel_boxes: string[];
|
|
52
42
|
};
|
|
43
|
+
export type UpdateNoteResult = {
|
|
44
|
+
id: string;
|
|
45
|
+
updated: boolean;
|
|
46
|
+
tags: string[];
|
|
47
|
+
zettelBoxIds: string[];
|
|
48
|
+
};
|
|
53
49
|
export declare function searchNotes(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesOptions): Promise<SearchedNote[]>;
|
|
54
50
|
export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
|
|
51
|
+
export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
|
|
55
52
|
export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
|
|
56
53
|
export declare function createNote(db: AbstractPowerSyncDatabase, input: {
|
|
57
54
|
id: string;
|
|
@@ -67,7 +64,15 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
|
|
|
67
64
|
id: string;
|
|
68
65
|
}>;
|
|
69
66
|
export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
|
|
67
|
+
export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
|
|
68
|
+
export declare function updateNote(db: AbstractPowerSyncDatabase, input: {
|
|
69
|
+
id: string;
|
|
70
|
+
tags?: string[];
|
|
71
|
+
zettelBoxIds?: string[];
|
|
72
|
+
}): Promise<UpdateNoteResult>;
|
|
73
|
+
export declare function assertNotesUpdatable(db: AbstractPowerSyncDatabase, noteIds: string[]): Promise<void>;
|
|
70
74
|
export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
71
75
|
export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
76
|
+
export declare function resolveZettelBoxIdsForUpdate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
72
77
|
export declare function softDeleteNote(db: AbstractPowerSyncDatabase, id: string): Promise<void>;
|
|
73
78
|
export {};
|
|
@@ -4,6 +4,7 @@ import { tokenizeWithJieba } from '../../utils/tokenize.js';
|
|
|
4
4
|
import { listBoxes } from '../boxes/repo.js';
|
|
5
5
|
import { listTags } from '../tags/repo.js';
|
|
6
6
|
import { buildNoteSearchSqlFilter } from './searchSql.js';
|
|
7
|
+
const MAX_SEARCH_SUMMARY_CHARS = 200;
|
|
7
8
|
export async function searchNotes(db, query, options) {
|
|
8
9
|
const normalizedQuery = query.trim();
|
|
9
10
|
const tagFilter = buildTagFilter(options.tagsExpression, 'n');
|
|
@@ -74,10 +75,9 @@ export async function searchNotes(db, query, options) {
|
|
|
74
75
|
n.title,
|
|
75
76
|
n.summary,
|
|
76
77
|
n.content_text,
|
|
78
|
+
n.tags,
|
|
77
79
|
n.zettel_boxes,
|
|
78
|
-
n.
|
|
79
|
-
n.is_del,
|
|
80
|
-
n.version
|
|
80
|
+
n.created_at
|
|
81
81
|
FROM c_note n
|
|
82
82
|
WHERE ${whereClauses.join('\n AND ')}
|
|
83
83
|
ORDER BY COALESCE(n.updated_at, n.created_at) DESC
|
|
@@ -109,10 +109,9 @@ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, ze
|
|
|
109
109
|
n.title,
|
|
110
110
|
n.summary,
|
|
111
111
|
n.content_text,
|
|
112
|
+
n.tags,
|
|
112
113
|
n.zettel_boxes,
|
|
113
|
-
n.
|
|
114
|
-
n.is_del,
|
|
115
|
-
n.version
|
|
114
|
+
n.created_at
|
|
116
115
|
FROM note_local_fts f
|
|
117
116
|
JOIN c_note n ON n.id = f.note_id
|
|
118
117
|
WHERE ${whereClauses.join('\n AND ')}
|
|
@@ -131,9 +130,8 @@ async function enrichSearchRows(db, rows) {
|
|
|
131
130
|
id: row.id,
|
|
132
131
|
title: row.title?.trim() ?? '',
|
|
133
132
|
summary,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
version: row.version,
|
|
133
|
+
tags: parseStoredTags(row.tags),
|
|
134
|
+
created_at: row.created_at,
|
|
137
135
|
zettel_boxes: zettelBoxes,
|
|
138
136
|
};
|
|
139
137
|
});
|
|
@@ -141,9 +139,16 @@ async function enrichSearchRows(db, rows) {
|
|
|
141
139
|
function resolveSummary(summary, contentText) {
|
|
142
140
|
const summaryValue = summary?.trim() ?? '';
|
|
143
141
|
if (summaryValue.length > 0) {
|
|
144
|
-
return summaryValue;
|
|
142
|
+
return truncateSearchSummary(summaryValue);
|
|
145
143
|
}
|
|
146
|
-
return contentText?.trim() ?? '';
|
|
144
|
+
return truncateSearchSummary(contentText?.trim() ?? '');
|
|
145
|
+
}
|
|
146
|
+
function truncateSearchSummary(value) {
|
|
147
|
+
const chars = Array.from(value);
|
|
148
|
+
if (chars.length <= MAX_SEARCH_SUMMARY_CHARS) {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
return chars.slice(0, MAX_SEARCH_SUMMARY_CHARS).join('');
|
|
147
152
|
}
|
|
148
153
|
function parseZettelBoxIds(raw) {
|
|
149
154
|
const value = raw?.trim();
|
|
@@ -445,53 +450,73 @@ export async function getNote(db, id) {
|
|
|
445
450
|
WHERE id = ?
|
|
446
451
|
`, [id]);
|
|
447
452
|
}
|
|
448
|
-
|
|
449
|
-
const note = await db.getOptional(`
|
|
450
|
-
SELECT
|
|
451
|
-
id,
|
|
452
|
-
content,
|
|
453
|
-
audios,
|
|
454
|
-
images,
|
|
455
|
-
created_at,
|
|
456
|
-
updated_at,
|
|
457
|
-
title,
|
|
458
|
-
type,
|
|
459
|
-
source,
|
|
460
|
-
resource_id,
|
|
461
|
-
tags,
|
|
462
|
-
is_del,
|
|
463
|
-
content_md,
|
|
464
|
-
format_type,
|
|
465
|
-
zettel_boxes AS zettel_boxes_raw
|
|
466
|
-
FROM c_note
|
|
467
|
-
WHERE id = ?
|
|
468
|
-
`, [id]);
|
|
469
|
-
if (!note) {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
453
|
+
function mapNoteDetailRow(note, boxNameMap) {
|
|
472
454
|
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
473
|
-
const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
|
|
474
455
|
const zettelBoxes = boxIds
|
|
475
456
|
.map((boxId) => boxNameMap.get(boxId))
|
|
476
457
|
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
477
458
|
return {
|
|
478
459
|
id: note.id,
|
|
479
|
-
content: note.content,
|
|
480
|
-
audios: note.audios,
|
|
481
|
-
images: note.images,
|
|
482
460
|
created_at: note.created_at,
|
|
483
|
-
updated_at: note.updated_at,
|
|
484
461
|
title: note.title,
|
|
485
462
|
type: note.type,
|
|
486
463
|
source: note.source,
|
|
487
|
-
resource_id: note.resource_id,
|
|
488
464
|
tags: note.tags,
|
|
489
465
|
is_del: note.is_del,
|
|
490
466
|
content_md: note.content_md,
|
|
491
|
-
format_type: note.format_type,
|
|
492
467
|
zettel_boxes: zettelBoxes,
|
|
493
468
|
};
|
|
494
469
|
}
|
|
470
|
+
export async function getNoteDetails(db, ids) {
|
|
471
|
+
const targetIds = Array.from(new Set(ids
|
|
472
|
+
.map((id) => id.trim())
|
|
473
|
+
.filter((id) => id.length > 0)));
|
|
474
|
+
if (targetIds.length === 0) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const noteRowsById = new Map();
|
|
478
|
+
const chunkSize = 500;
|
|
479
|
+
for (let index = 0; index < targetIds.length; index += chunkSize) {
|
|
480
|
+
const batch = targetIds.slice(index, index + chunkSize);
|
|
481
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
482
|
+
const rows = await db.getAll(`
|
|
483
|
+
SELECT
|
|
484
|
+
id,
|
|
485
|
+
created_at,
|
|
486
|
+
title,
|
|
487
|
+
type,
|
|
488
|
+
source,
|
|
489
|
+
tags,
|
|
490
|
+
is_del,
|
|
491
|
+
content_md,
|
|
492
|
+
zettel_boxes AS zettel_boxes_raw
|
|
493
|
+
FROM c_note
|
|
494
|
+
WHERE id IN (${placeholders})
|
|
495
|
+
`, batch);
|
|
496
|
+
for (const row of rows) {
|
|
497
|
+
const id = row.id?.trim() ?? '';
|
|
498
|
+
if (!id || noteRowsById.has(id)) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
noteRowsById.set(id, row);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
|
|
505
|
+
const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
|
|
506
|
+
const details = [];
|
|
507
|
+
for (const id of targetIds) {
|
|
508
|
+
const row = noteRowsById.get(id);
|
|
509
|
+
if (!row) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
details.push(mapNoteDetailRow(row, boxNameMap));
|
|
513
|
+
}
|
|
514
|
+
return details;
|
|
515
|
+
}
|
|
516
|
+
export async function getNoteDetail(db, id) {
|
|
517
|
+
const details = await getNoteDetails(db, [id]);
|
|
518
|
+
return details[0] ?? null;
|
|
519
|
+
}
|
|
495
520
|
export async function createNote(db, input) {
|
|
496
521
|
const userId = input.userId.trim();
|
|
497
522
|
if (!userId) {
|
|
@@ -585,13 +610,23 @@ function normalizeLookupKey(value) {
|
|
|
585
610
|
return value.trim().toLowerCase();
|
|
586
611
|
}
|
|
587
612
|
function buildZettelBoxRetryQuestion(intent, type) {
|
|
588
|
-
const command = intent === 'search'
|
|
613
|
+
const command = intent === 'search'
|
|
614
|
+
? 'dino note search'
|
|
615
|
+
: intent === 'update'
|
|
616
|
+
? 'dino note update'
|
|
617
|
+
: 'dino note create';
|
|
589
618
|
if (type === 'ambiguous') {
|
|
590
619
|
return `Do you want to specify unique box names (or IDs) and retry \`${command}\`?`;
|
|
591
620
|
}
|
|
592
621
|
return `Do you want to add these missing boxes and retry \`${command}\`?`;
|
|
593
622
|
}
|
|
594
623
|
export async function validateTagsForCreate(db, requestedTags) {
|
|
624
|
+
return validateTagsForWrite(db, requestedTags, 'dino note create');
|
|
625
|
+
}
|
|
626
|
+
export async function validateTagsForUpdate(db, requestedTags) {
|
|
627
|
+
return validateTagsForWrite(db, requestedTags, 'dino note update');
|
|
628
|
+
}
|
|
629
|
+
async function validateTagsForWrite(db, requestedTags, retryCommand) {
|
|
595
630
|
if (requestedTags.length === 0) {
|
|
596
631
|
return [];
|
|
597
632
|
}
|
|
@@ -627,18 +662,166 @@ export async function validateTagsForCreate(db, requestedTags) {
|
|
|
627
662
|
missing,
|
|
628
663
|
knownCount: tags.length,
|
|
629
664
|
knownSample: tags.slice(0, 20),
|
|
630
|
-
question:
|
|
665
|
+
question: `Do you want to add these missing tags and retry \`${retryCommand}\`?`,
|
|
631
666
|
},
|
|
632
667
|
});
|
|
633
668
|
}
|
|
634
669
|
return resolved;
|
|
635
670
|
}
|
|
671
|
+
function parseStoredTags(raw) {
|
|
672
|
+
if (!raw) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const parsed = JSON.parse(raw);
|
|
677
|
+
if (!Array.isArray(parsed)) {
|
|
678
|
+
return [];
|
|
679
|
+
}
|
|
680
|
+
const result = [];
|
|
681
|
+
const seen = new Set();
|
|
682
|
+
for (const value of parsed) {
|
|
683
|
+
const normalized = String(value).trim();
|
|
684
|
+
const key = normalizeLookupKey(normalized);
|
|
685
|
+
if (!key || seen.has(key)) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
seen.add(key);
|
|
689
|
+
result.push(normalized);
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function normalizeDistinctStrings(values) {
|
|
698
|
+
return Array.from(new Set(values
|
|
699
|
+
.map((value) => value.trim())
|
|
700
|
+
.filter((value) => value.length > 0)));
|
|
701
|
+
}
|
|
702
|
+
function sameTagSet(left, right) {
|
|
703
|
+
if (left.length !== right.length) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
const leftKeys = left.map(normalizeLookupKey).sort();
|
|
707
|
+
const rightKeys = right.map(normalizeLookupKey).sort();
|
|
708
|
+
return leftKeys.every((value, index) => value === rightKeys[index]);
|
|
709
|
+
}
|
|
710
|
+
function sameStringSet(left, right) {
|
|
711
|
+
if (left.length !== right.length) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
const leftSorted = [...left].sort();
|
|
715
|
+
const rightSorted = [...right].sort();
|
|
716
|
+
return leftSorted.every((value, index) => value === rightSorted[index]);
|
|
717
|
+
}
|
|
718
|
+
export async function updateNote(db, input) {
|
|
719
|
+
const id = input.id.trim();
|
|
720
|
+
if (!id) {
|
|
721
|
+
throw new DinoxError('Note id is required');
|
|
722
|
+
}
|
|
723
|
+
const shouldUpdateTags = Array.isArray(input.tags);
|
|
724
|
+
const shouldUpdateBoxes = Array.isArray(input.zettelBoxIds);
|
|
725
|
+
if (!shouldUpdateTags && !shouldUpdateBoxes) {
|
|
726
|
+
throw new DinoxError('At least one field is required: tags or boxes');
|
|
727
|
+
}
|
|
728
|
+
const existing = await db.getOptional(`
|
|
729
|
+
SELECT id, tags, zettel_boxes, is_del
|
|
730
|
+
FROM c_note
|
|
731
|
+
WHERE id = ?
|
|
732
|
+
`, [id]);
|
|
733
|
+
if (!existing) {
|
|
734
|
+
throw new DinoxError(`Note not found: ${id}`);
|
|
735
|
+
}
|
|
736
|
+
if (typeof existing.is_del === 'number' && existing.is_del !== 0) {
|
|
737
|
+
throw new DinoxError(`Note has been deleted: ${id}`);
|
|
738
|
+
}
|
|
739
|
+
const currentTags = parseStoredTags(existing.tags);
|
|
740
|
+
const currentZettelBoxIds = parseZettelBoxIds(existing.zettel_boxes);
|
|
741
|
+
const nextTags = shouldUpdateTags ? normalizeDistinctStrings(input.tags) : currentTags;
|
|
742
|
+
const nextZettelBoxIds = shouldUpdateBoxes ? normalizeDistinctStrings(input.zettelBoxIds) : currentZettelBoxIds;
|
|
743
|
+
const tagsChanged = shouldUpdateTags && !sameTagSet(currentTags, nextTags);
|
|
744
|
+
const boxesChanged = shouldUpdateBoxes && !sameStringSet(currentZettelBoxIds, nextZettelBoxIds);
|
|
745
|
+
if (!tagsChanged && !boxesChanged) {
|
|
746
|
+
return {
|
|
747
|
+
id,
|
|
748
|
+
updated: false,
|
|
749
|
+
tags: currentTags,
|
|
750
|
+
zettelBoxIds: currentZettelBoxIds,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const setParts = [];
|
|
754
|
+
const params = [];
|
|
755
|
+
if (tagsChanged) {
|
|
756
|
+
setParts.push('tags = ?');
|
|
757
|
+
params.push(JSON.stringify(nextTags));
|
|
758
|
+
}
|
|
759
|
+
if (boxesChanged) {
|
|
760
|
+
setParts.push('zettel_boxes = ?');
|
|
761
|
+
params.push(JSON.stringify(nextZettelBoxIds));
|
|
762
|
+
}
|
|
763
|
+
setParts.push('updated_at = ?');
|
|
764
|
+
params.push(nowIsoUtc());
|
|
765
|
+
setParts.push('version = COALESCE(version, 0) + 1');
|
|
766
|
+
params.push(id);
|
|
767
|
+
await db.execute(`
|
|
768
|
+
UPDATE c_note
|
|
769
|
+
SET ${setParts.join(',\n ')}
|
|
770
|
+
WHERE id = ?
|
|
771
|
+
`, params);
|
|
772
|
+
return {
|
|
773
|
+
id,
|
|
774
|
+
updated: true,
|
|
775
|
+
tags: nextTags,
|
|
776
|
+
zettelBoxIds: nextZettelBoxIds,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
export async function assertNotesUpdatable(db, noteIds) {
|
|
780
|
+
const ids = Array.from(new Set(noteIds
|
|
781
|
+
.map((id) => id.trim())
|
|
782
|
+
.filter((id) => id.length > 0)));
|
|
783
|
+
if (ids.length === 0) {
|
|
784
|
+
throw new DinoxError('At least one note id is required');
|
|
785
|
+
}
|
|
786
|
+
const existing = new Map();
|
|
787
|
+
const chunkSize = 500;
|
|
788
|
+
for (let index = 0; index < ids.length; index += chunkSize) {
|
|
789
|
+
const batch = ids.slice(index, index + chunkSize);
|
|
790
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
791
|
+
const rows = await db.getAll(`
|
|
792
|
+
SELECT id, is_del
|
|
793
|
+
FROM c_note
|
|
794
|
+
WHERE id IN (${placeholders})
|
|
795
|
+
`, batch);
|
|
796
|
+
for (const row of rows) {
|
|
797
|
+
const id = row.id?.trim();
|
|
798
|
+
if (!id) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
existing.set(id, row.is_del ?? null);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const missing = ids.filter((id) => !existing.has(id));
|
|
805
|
+
if (missing.length > 0) {
|
|
806
|
+
throw new DinoxError(`Notes not found: ${missing.join(', ')}`);
|
|
807
|
+
}
|
|
808
|
+
const deleted = ids.filter((id) => {
|
|
809
|
+
const isDel = existing.get(id);
|
|
810
|
+
return typeof isDel === 'number' && isDel !== 0;
|
|
811
|
+
});
|
|
812
|
+
if (deleted.length > 0) {
|
|
813
|
+
throw new DinoxError(`Notes have been deleted: ${deleted.join(', ')}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
636
816
|
export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
|
|
637
817
|
return resolveZettelBoxIdsByName(db, requestedNames, 'create');
|
|
638
818
|
}
|
|
639
819
|
export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
|
|
640
820
|
return resolveZettelBoxIdsByName(db, requestedNames, 'search');
|
|
641
821
|
}
|
|
822
|
+
export async function resolveZettelBoxIdsForUpdate(db, requestedNames) {
|
|
823
|
+
return resolveZettelBoxIdsByName(db, requestedNames, 'update');
|
|
824
|
+
}
|
|
642
825
|
async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
|
|
643
826
|
if (requestedNames.length === 0) {
|
|
644
827
|
return [];
|