@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 CHANGED
@@ -342,17 +342,23 @@ dino box add "你的卡片盒"
342
342
 
343
343
  | 参数 | 必填 | 说明 |
344
344
  | --- | --- | --- |
345
- | `<id>` | | 笔记 ID |
345
+ | `[id]` | | 单个笔记 ID(可与 `--ids` 组合,去重后批量更新)。 |
346
346
 
347
347
  支持全局参数:`--json`、`--offline`、`--sync-timeout`
348
348
 
349
- ### `dino note detail <id>`
349
+ ### `dino note detail [id]`
350
350
 
351
- 按 ID 获取笔记完整详情。
351
+ 按 ID 获取笔记完整详情,支持批量读取。
352
352
 
353
353
  | 参数 | 必填 | 说明 |
354
354
  | --- | --- | --- |
355
- | `<id>` | | 笔记 ID |
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, getNoteDetail, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
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('<id>', 'note id')
158
- .action(async (id, _options, command) => {
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 note = await getNoteDetail(db, id);
167
- if (!note) {
168
- throw new DinoxError(`Note not found: ${id}`);
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
- updated_at: string | null;
29
- is_del: number | null;
30
- version: number | null;
28
+ tags: string[];
29
+ created_at: string | null;
31
30
  zettel_boxes: string[];
32
31
  };
33
- type NoteDetailRow = {
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.updated_at,
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.updated_at,
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
- updated_at: row.updated_at,
135
- is_del: row.is_del,
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
- export async function getNoteDetail(db, id) {
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' ? 'dino note search' : 'dino note create';
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: 'Do you want to add these missing tags and retry `dino note create`?',
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 [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {