@dinoxx/dinox-cli 1.0.19 → 1.0.21

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.
Files changed (88) hide show
  1. package/README.md +756 -472
  2. package/dist/auth/userInfo.d.ts +10 -1
  3. package/dist/auth/userInfo.js +66 -17
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +29 -14
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +45 -16
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/storage/repo.d.ts +1 -1
  55. package/dist/commands/storage/repo.js +20 -1
  56. package/dist/commands/sync.js +4 -2
  57. package/dist/commands/tags/content.d.ts +32 -0
  58. package/dist/commands/tags/content.js +243 -0
  59. package/dist/commands/tags/index.js +285 -49
  60. package/dist/commands/tags/model.d.ts +38 -0
  61. package/dist/commands/tags/model.js +100 -0
  62. package/dist/commands/tags/operations.d.ts +105 -0
  63. package/dist/commands/tags/operations.js +438 -0
  64. package/dist/commands/tags/repo.d.ts +5 -1
  65. package/dist/commands/tags/repo.js +142 -218
  66. package/dist/commands/tags/store.d.ts +28 -0
  67. package/dist/commands/tags/store.js +166 -0
  68. package/dist/commands/todo/index.js +30 -61
  69. package/dist/commands/writeDryRun.d.ts +25 -0
  70. package/dist/commands/writeDryRun.js +31 -1
  71. package/dist/config/paths.d.ts +2 -1
  72. package/dist/config/paths.js +11 -1
  73. package/dist/config/resolve.js +9 -7
  74. package/dist/daemon/index.d.ts +2 -0
  75. package/dist/daemon/index.js +176 -58
  76. package/dist/powersync/runtime.d.ts +2 -1
  77. package/dist/powersync/runtime.js +15 -3
  78. package/dist/powersync/schema/content.d.ts +2 -0
  79. package/dist/powersync/schema/content.js +2 -0
  80. package/dist/powersync/schema/index.d.ts +2 -0
  81. package/dist/skills/check.d.ts +1 -1
  82. package/dist/skills/check.js +27 -2
  83. package/dist/utils/authToken.d.ts +2 -0
  84. package/dist/utils/authToken.js +7 -2
  85. package/dist/utils/noteContentEngine.d.ts +333 -0
  86. package/dist/utils/noteContentEngine.js +740 -0
  87. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  88. package/package.json +4 -8
@@ -1,117 +1,53 @@
1
1
  import { generateId } from '../../utils/id.js';
2
2
  import { DinoxError } from '../../utils/errors.js';
3
- function canonicalizeTagPath(value) {
4
- return value
5
- .split(/[\\/]+/g)
6
- .map((segment) => segment.trim())
7
- .filter(Boolean)
8
- .join('/');
9
- }
10
- async function getTagNodeColumns(db) {
11
- const rows = await db.getAll('PRAGMA table_info(c_tag_node)', []);
12
- return new Set(rows.map((row) => row.name).filter(Boolean));
13
- }
14
- async function loadTagNodes(db, columnNames) {
15
- const fullPathExpr = columnNames.has('full_path')
16
- ? (columnNames.has('path_key')
17
- ? `TRIM(COALESCE(full_path, path_key, ''))`
18
- : `TRIM(COALESCE(full_path, ''))`)
19
- : `TRIM(COALESCE(path_key, ''))`;
20
- const pathKeyExpr = columnNames.has('path_key')
21
- ? (columnNames.has('full_path')
22
- ? `TRIM(COALESCE(path_key, full_path, ''))`
23
- : `TRIM(COALESCE(path_key, ''))`)
24
- : `TRIM(COALESCE(full_path, ''))`;
25
- const userIdExpr = columnNames.has('user_id')
26
- ? `TRIM(COALESCE(user_id, ''))`
27
- : `''`;
28
- const emojiExpr = columnNames.has('emoji')
29
- ? `TRIM(COALESCE(emoji, ''))`
30
- : `''`;
31
- const parentIdExpr = columnNames.has('parent_id') ? 'parent_id' : 'NULL';
32
- const depthExpr = columnNames.has('depth') ? 'depth' : 'NULL';
33
- const isActualExpr = columnNames.has('is_actual') ? 'is_actual' : 'NULL';
34
- const isDelExpr = columnNames.has('is_del') ? 'is_del' : 'NULL';
35
- const updatedOrderExpr = columnNames.has('updated_at') ? 'updated_at DESC,' : '';
36
- const createdOrderExpr = columnNames.has('created_at') ? 'created_at DESC,' : '';
37
- const rows = await db.getAll(`
38
- SELECT
39
- TRIM(COALESCE(id, '')) AS id,
40
- ${fullPathExpr} AS full_path,
41
- ${pathKeyExpr} AS path_key,
42
- ${parentIdExpr} AS parent_id,
43
- ${depthExpr} AS depth,
44
- ${isActualExpr} AS is_actual,
45
- ${isDelExpr} AS is_del,
46
- ${userIdExpr} AS user_id,
47
- ${emojiExpr} AS emoji
48
- FROM c_tag_node
49
- WHERE ${fullPathExpr} <> ''
50
- ORDER BY
51
- CASE WHEN is_del IS NULL OR is_del = 0 THEN 0 ELSE 1 END ASC,
52
- CASE WHEN is_actual IS NULL THEN 0 ELSE is_actual END DESC,
53
- ${updatedOrderExpr}
54
- ${createdOrderExpr}
55
- id DESC
56
- `, []);
57
- const nodes = new Map();
58
- for (const row of rows) {
59
- const id = row.id?.trim() ?? '';
60
- if (!id) {
61
- continue;
62
- }
63
- const fullPath = canonicalizeTagPath(row.full_path ?? row.path_key ?? '');
64
- if (!fullPath) {
65
- continue;
66
- }
67
- if (nodes.has(fullPath)) {
68
- continue;
3
+ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
4
+ import { MAX_TAG_EMOJI_LENGTH, MAX_TAG_PATH_LENGTH, MAX_TAG_SEGMENT_LENGTH, assertMaxLength, canonicalizeTagPath, getLeafName, normalizeTagLookupKey, trimString, } from './model.js';
5
+ import { assertCanWriteTag, getTableColumns, loadTagNodes, } from './store.js';
6
+ export { canonicalizeTagPath } from './model.js';
7
+ function tagNodeMapByPath(nodes) {
8
+ const map = new Map();
9
+ for (const node of nodes) {
10
+ const key = normalizeTagLookupKey(node.fullPath);
11
+ if (!map.has(key)) {
12
+ map.set(key, node);
69
13
  }
70
- nodes.set(fullPath, {
71
- id,
72
- fullPath,
73
- pathKey: canonicalizeTagPath(row.path_key ?? fullPath) || fullPath,
74
- parentId: row.parent_id ?? null,
75
- depth: typeof row.depth === 'number' ? row.depth : null,
76
- isActual: typeof row.is_actual === 'number' ? row.is_actual : null,
77
- isDel: typeof row.is_del === 'number' ? row.is_del : null,
78
- userId: row.user_id?.trim() ?? '',
79
- emoji: row.emoji?.trim() ?? '',
80
- });
81
14
  }
82
- return nodes;
15
+ return map;
16
+ }
17
+ function assertAddTagInput(input) {
18
+ const userId = trimString(input.userId);
19
+ const emoji = trimString(input.emoji);
20
+ const tag = canonicalizeTagPath(input.name);
21
+ if (!tag) {
22
+ throw new DinoxError('Tag name is required');
23
+ }
24
+ assertMaxLength('Tag path', tag, MAX_TAG_PATH_LENGTH);
25
+ assertMaxLength('Tag name', getLeafName(tag), MAX_TAG_SEGMENT_LENGTH);
26
+ assertMaxLength('Tag emoji', emoji, MAX_TAG_EMOJI_LENGTH);
27
+ return { tag, userId, emoji };
83
28
  }
84
- export async function listTags(db) {
29
+ export async function listTags(db, input = {}) {
30
+ const userId = trimString(input.userId);
31
+ const userFilter = userId
32
+ ? `AND (TRIM(COALESCE(user_id, '')) = ? OR TRIM(COALESCE(user_id, '')) = '')`
33
+ : '';
85
34
  const rows = await db.getAll(`
86
35
  SELECT DISTINCT TRIM(COALESCE(full_path, path_key, '')) AS tag
87
36
  FROM c_tag_node
88
37
  WHERE (is_del IS NULL OR is_del = 0)
89
38
  AND (is_actual IS NULL OR is_actual != 0)
90
39
  AND TRIM(COALESCE(full_path, path_key, '')) <> ''
40
+ ${userFilter}
91
41
  ORDER BY tag COLLATE NOCASE ASC
92
- `, []);
42
+ `, userId ? [userId] : []);
93
43
  return rows.map((row) => row.tag?.trim() ?? '').filter(Boolean);
94
44
  }
95
45
  export async function addTag(db, input) {
96
- const userId = input.userId.trim();
97
- const emoji = typeof input.emoji === 'string' ? input.emoji.trim() : '';
98
- const tag = canonicalizeTagPath(input.name);
99
- if (!tag) {
100
- throw new DinoxError('Tag name is required');
101
- }
102
- const columnNames = await getTagNodeColumns(db);
103
- if (!columnNames.has('id')) {
104
- throw new DinoxError('c_tag_node table is missing required column: id');
105
- }
106
- if (!columnNames.has('full_path') && !columnNames.has('path_key')) {
107
- throw new DinoxError('c_tag_node table is missing required column: full_path or path_key');
108
- }
109
- const requiredUserColumns = ['user_id'].filter((column) => columnNames.has(column));
110
- if (requiredUserColumns.length > 0 && !userId) {
111
- throw new DinoxError(`c_tag_node insert requires ${requiredUserColumns.join(', ')}. Missing persisted userId. Run \`dino auth login\` first.`);
112
- }
113
- const nodesByPath = await loadTagNodes(db, columnNames);
114
- const existingLeaf = nodesByPath.get(tag);
46
+ const { tag, userId, emoji } = assertAddTagInput(input);
47
+ const columnNames = await getTableColumns(db, 'c_tag_node');
48
+ assertCanWriteTag(columnNames, userId, 'insert');
49
+ const nodesByPath = tagNodeMapByPath(await loadTagNodes(db, columnNames, { userId, dedupeByPath: true }));
50
+ const existingLeaf = nodesByPath.get(normalizeTagLookupKey(tag));
115
51
  if (existingLeaf && existingLeaf.isDel !== 1 && existingLeaf.isActual === 1) {
116
52
  throw new DinoxError(`Tag already exists: ${tag}`, {
117
53
  details: {
@@ -128,113 +64,115 @@ export async function addTag(db, input) {
128
64
  let restoredNodes = 0;
129
65
  let parentId = null;
130
66
  let leafId = '';
131
- for (let i = 0; i < segments.length; i += 1) {
132
- const path = segments.slice(0, i + 1).join('/');
133
- const isLeaf = i === segments.length - 1;
134
- const existing = nodesByPath.get(path);
135
- if (existing) {
136
- const assignments = [];
137
- const params = [];
138
- const setValue = (column, value) => {
139
- if (!columnNames.has(column)) {
140
- return;
141
- }
142
- assignments.push(`${column} = ?`);
143
- params.push(value);
144
- };
145
- if (existing.isDel === 1) {
146
- setValue('is_del', 0);
147
- }
148
- if (isLeaf && existing.isActual !== 1) {
149
- setValue('is_actual', 1);
150
- }
151
- if (columnNames.has('updated_at')) {
152
- setValue('updated_at', timestamp);
153
- }
154
- if (columnNames.has('user_id') && !existing.userId) {
155
- setValue('user_id', userId);
156
- }
157
- if (columnNames.has('parent_id') && existing.parentId == null && parentId !== null) {
158
- setValue('parent_id', parentId);
159
- }
160
- if (columnNames.has('depth') && existing.depth == null) {
161
- setValue('depth', i);
162
- }
163
- if (columnNames.has('full_path') && existing.fullPath !== path) {
164
- setValue('full_path', path);
165
- }
166
- if (columnNames.has('path_key') && existing.pathKey !== path) {
167
- setValue('path_key', path);
168
- }
169
- if (isLeaf && emoji && columnNames.has('emoji') && existing.emoji !== emoji) {
170
- setValue('emoji', emoji);
171
- }
172
- if (assignments.length > 0) {
173
- params.push(existing.id);
174
- await db.execute(`
175
- UPDATE c_tag_node
176
- SET ${assignments.join(', ')}
177
- WHERE id = ?
178
- `, params);
67
+ await withPowerSyncWriteTransaction(db, async (tx) => {
68
+ for (let i = 0; i < segments.length; i += 1) {
69
+ const path = segments.slice(0, i + 1).join('/');
70
+ const isLeaf = i === segments.length - 1;
71
+ const existing = nodesByPath.get(normalizeTagLookupKey(path));
72
+ if (existing) {
73
+ const assignments = [];
74
+ const params = [];
75
+ const setValue = (column, value) => {
76
+ if (!columnNames.has(column)) {
77
+ return;
78
+ }
79
+ assignments.push(`${column} = ?`);
80
+ params.push(value);
81
+ };
179
82
  if (existing.isDel === 1) {
180
- restoredNodes += 1;
181
- existing.isDel = 0;
83
+ setValue('is_del', 0);
182
84
  }
183
85
  if (isLeaf && existing.isActual !== 1) {
184
- existing.isActual = 1;
86
+ setValue('is_actual', 1);
87
+ }
88
+ if (columnNames.has('updated_at')) {
89
+ setValue('updated_at', timestamp);
90
+ }
91
+ if (columnNames.has('user_id') && !existing.userId) {
92
+ setValue('user_id', userId);
185
93
  }
94
+ if (columnNames.has('parent_id') && existing.parentId == null && parentId !== null) {
95
+ setValue('parent_id', parentId);
96
+ }
97
+ if (columnNames.has('depth') && existing.depth == null) {
98
+ setValue('depth', i);
99
+ }
100
+ if (columnNames.has('full_path') && existing.fullPath !== path) {
101
+ setValue('full_path', path);
102
+ }
103
+ if (columnNames.has('path_key') && existing.pathKey !== path) {
104
+ setValue('path_key', path);
105
+ }
106
+ if (isLeaf && emoji && columnNames.has('emoji') && existing.emoji !== emoji) {
107
+ setValue('emoji', emoji);
108
+ }
109
+ if (assignments.length > 0) {
110
+ params.push(existing.id);
111
+ await tx.execute(`
112
+ UPDATE c_tag_node
113
+ SET ${assignments.join(', ')}
114
+ WHERE id = ?
115
+ `, params);
116
+ if (existing.isDel === 1) {
117
+ restoredNodes += 1;
118
+ existing.isDel = 0;
119
+ }
120
+ if (isLeaf && existing.isActual !== 1) {
121
+ existing.isActual = 1;
122
+ }
123
+ }
124
+ parentId = existing.id;
125
+ if (isLeaf) {
126
+ leafId = existing.id;
127
+ }
128
+ continue;
186
129
  }
187
- parentId = existing.id;
130
+ const id = generateId();
131
+ const insertEntries = [];
132
+ const pushInsert = (column, value) => {
133
+ if (!columnNames.has(column)) {
134
+ return;
135
+ }
136
+ insertEntries.push([column, value]);
137
+ };
138
+ pushInsert('id', id);
139
+ pushInsert('user_id', userId);
140
+ pushInsert('emoji', isLeaf && emoji ? emoji : null);
141
+ pushInsert('full_path', path);
142
+ pushInsert('path_key', path);
143
+ pushInsert('parent_id', parentId);
144
+ pushInsert('depth', i);
145
+ pushInsert('is_actual', isLeaf ? 1 : 0);
146
+ pushInsert('is_del', 0);
147
+ pushInsert('created_at', timestamp);
148
+ pushInsert('updated_at', timestamp);
149
+ pushInsert('meta', '{}');
150
+ const insertColumns = insertEntries.map(([column]) => column);
151
+ const placeholders = insertColumns.map(() => '?').join(', ');
152
+ const insertParams = insertEntries.map(([, value]) => value);
153
+ await tx.execute(`
154
+ INSERT INTO c_tag_node (${insertColumns.join(', ')})
155
+ VALUES (${placeholders})
156
+ `, insertParams);
157
+ const node = {
158
+ id,
159
+ fullPath: path,
160
+ pathKey: path,
161
+ parentId,
162
+ depth: i,
163
+ isActual: isLeaf ? 1 : 0,
164
+ isDel: 0,
165
+ userId,
166
+ emoji: isLeaf && emoji ? emoji : '',
167
+ };
168
+ nodesByPath.set(normalizeTagLookupKey(path), node);
169
+ createdNodes += 1;
170
+ parentId = id;
188
171
  if (isLeaf) {
189
- leafId = existing.id;
172
+ leafId = id;
190
173
  }
191
- continue;
192
- }
193
- const id = generateId();
194
- const insertEntries = [];
195
- const pushInsert = (column, value) => {
196
- if (!columnNames.has(column)) {
197
- return;
198
- }
199
- insertEntries.push([column, value]);
200
- };
201
- pushInsert('id', id);
202
- pushInsert('user_id', userId);
203
- pushInsert('emoji', isLeaf && emoji ? emoji : null);
204
- pushInsert('full_path', path);
205
- pushInsert('path_key', path);
206
- pushInsert('parent_id', parentId);
207
- pushInsert('depth', i);
208
- pushInsert('is_actual', isLeaf ? 1 : 0);
209
- pushInsert('is_del', 0);
210
- pushInsert('created_at', timestamp);
211
- pushInsert('updated_at', timestamp);
212
- pushInsert('meta', '{}');
213
- const insertColumns = insertEntries.map(([column]) => column);
214
- const placeholders = insertColumns.map(() => '?').join(', ');
215
- const insertParams = insertEntries.map(([, value]) => value);
216
- await db.execute(`
217
- INSERT INTO c_tag_node (${insertColumns.join(', ')})
218
- VALUES (${placeholders})
219
- `, insertParams);
220
- const node = {
221
- id,
222
- fullPath: path,
223
- pathKey: path,
224
- parentId,
225
- depth: i,
226
- isActual: isLeaf ? 1 : 0,
227
- isDel: 0,
228
- userId,
229
- emoji: isLeaf && emoji ? emoji : '',
230
- };
231
- nodesByPath.set(path, node);
232
- createdNodes += 1;
233
- parentId = id;
234
- if (isLeaf) {
235
- leafId = id;
236
174
  }
237
- }
175
+ });
238
176
  if (!leafId) {
239
177
  throw new DinoxError(`Failed to create tag: ${tag}`);
240
178
  }
@@ -246,25 +184,11 @@ export async function addTag(db, input) {
246
184
  };
247
185
  }
248
186
  export async function inspectAddTag(db, input) {
249
- const userId = input.userId.trim();
250
- const emoji = typeof input.emoji === 'string' ? input.emoji.trim() : '';
251
- const tag = canonicalizeTagPath(input.name);
252
- if (!tag) {
253
- throw new DinoxError('Tag name is required');
254
- }
255
- const columnNames = await getTagNodeColumns(db);
256
- if (!columnNames.has('id')) {
257
- throw new DinoxError('c_tag_node table is missing required column: id');
258
- }
259
- if (!columnNames.has('full_path') && !columnNames.has('path_key')) {
260
- throw new DinoxError('c_tag_node table is missing required column: full_path or path_key');
261
- }
262
- const requiredUserColumns = ['user_id'].filter((column) => columnNames.has(column));
263
- if (requiredUserColumns.length > 0 && !userId) {
264
- throw new DinoxError(`c_tag_node insert requires ${requiredUserColumns.join(', ')}. Missing persisted userId. Run \`dino auth login\` first.`);
265
- }
266
- const nodesByPath = await loadTagNodes(db, columnNames);
267
- const existingLeaf = nodesByPath.get(tag);
187
+ const { tag, userId, emoji } = assertAddTagInput(input);
188
+ const columnNames = await getTableColumns(db, 'c_tag_node');
189
+ assertCanWriteTag(columnNames, userId, 'insert');
190
+ const nodesByPath = tagNodeMapByPath(await loadTagNodes(db, columnNames, { userId, dedupeByPath: true }));
191
+ const existingLeaf = nodesByPath.get(normalizeTagLookupKey(tag));
268
192
  if (existingLeaf && existingLeaf.isDel !== 1 && existingLeaf.isActual === 1) {
269
193
  throw new DinoxError(`Tag already exists: ${tag}`, {
270
194
  details: {
@@ -281,7 +205,7 @@ export async function inspectAddTag(db, input) {
281
205
  for (let i = 0; i < segments.length; i += 1) {
282
206
  const path = segments.slice(0, i + 1).join('/');
283
207
  const isLeaf = i === segments.length - 1;
284
- const existing = nodesByPath.get(path);
208
+ const existing = nodesByPath.get(normalizeTagLookupKey(path));
285
209
  if (!existing) {
286
210
  createdPaths.push(path);
287
211
  continue;
@@ -0,0 +1,28 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { MutableTagNode, TagListItem } from './model.js';
3
+ type TableName = 'c_tag_node' | 'c_note';
4
+ export declare function getTableColumns(db: AbstractPowerSyncDatabase, table: TableName): Promise<Set<string>>;
5
+ export declare function assertRequiredColumns(table: string, columnNames: Set<string>, required: string[]): void;
6
+ export declare function assertCanWriteTag(columnNames: Set<string>, userId: string, action: 'insert' | 'update'): void;
7
+ export declare function loadTagNodes(db: AbstractPowerSyncDatabase, columnNames: Set<string>, options?: {
8
+ userId?: string;
9
+ dedupeByPath?: boolean;
10
+ }): Promise<MutableTagNode[]>;
11
+ export declare function activeTagNodes(nodes: MutableTagNode[]): MutableTagNode[];
12
+ export declare function actualActiveTagNodes(nodes: MutableTagNode[]): MutableTagNode[];
13
+ export declare function tagNodeByPath(nodes: MutableTagNode[], options?: {
14
+ actualOnly?: boolean;
15
+ }): Map<string, MutableTagNode>;
16
+ export declare function requireActiveTagPath(nodes: MutableTagNode[], rawPath: string, label: string): MutableTagNode;
17
+ export declare function descendantsOf(nodes: MutableTagNode[], root: MutableTagNode, includeRoot?: boolean): MutableTagNode[];
18
+ export declare function toTagListItem(node: MutableTagNode): TagListItem;
19
+ export declare function assertWritableTagNodes(nodes: MutableTagNode[], ids: Set<string>, userId: string): void;
20
+ export declare function ensureNoTagPathConflict(nodes: MutableTagNode[], changes: Array<{
21
+ id: string;
22
+ toPath: string;
23
+ }>): void;
24
+ export declare function parentIdForPath(path: string, nodes: MutableTagNode[], changes: Array<{
25
+ id: string;
26
+ toPath: string;
27
+ }>): string | null;
28
+ export {};
@@ -0,0 +1,166 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { REQUIRED_TAG_NODE_COLUMNS, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, normalizeTagLookupKey, parentPathOf, trimString, } from './model.js';
3
+ export async function getTableColumns(db, table) {
4
+ const sql = table === 'c_tag_node' ? 'PRAGMA table_info(c_tag_node)' : 'PRAGMA table_info(c_note)';
5
+ const rows = await db.getAll(sql, []);
6
+ return new Set(rows.map((row) => trimString(row.name)).filter(Boolean));
7
+ }
8
+ export function assertRequiredColumns(table, columnNames, required) {
9
+ const missing = required.filter((column) => !columnNames.has(column));
10
+ if (missing.length > 0) {
11
+ throw new DinoxError(`${table} table is missing required columns: ${missing.join(', ')}`);
12
+ }
13
+ }
14
+ export function assertCanWriteTag(columnNames, userId, action) {
15
+ assertRequiredColumns('c_tag_node', columnNames, REQUIRED_TAG_NODE_COLUMNS);
16
+ if (!userId.trim()) {
17
+ throw new DinoxError(`c_tag_node ${action} requires user_id. Missing resolved userId. Run \`dino auth login\` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.`);
18
+ }
19
+ }
20
+ export async function loadTagNodes(db, columnNames, options = {}) {
21
+ const userId = trimString(options.userId);
22
+ const userFilter = userId
23
+ ? `AND (TRIM(COALESCE(user_id, '')) = ? OR TRIM(COALESCE(user_id, '')) = '')`
24
+ : '';
25
+ const rows = await db.getAll(`
26
+ SELECT
27
+ TRIM(COALESCE(id, '')) AS id,
28
+ TRIM(COALESCE(full_path, path_key, '')) AS full_path,
29
+ TRIM(COALESCE(path_key, full_path, '')) AS path_key,
30
+ TRIM(COALESCE(parent_id, '')) AS parent_id,
31
+ depth,
32
+ is_actual,
33
+ is_del,
34
+ TRIM(COALESCE(user_id, '')) AS user_id,
35
+ TRIM(COALESCE(emoji, '')) AS emoji
36
+ FROM c_tag_node
37
+ WHERE TRIM(COALESCE(id, '')) <> ''
38
+ AND TRIM(COALESCE(full_path, path_key, '')) <> ''
39
+ ${userFilter}
40
+ ORDER BY
41
+ CASE WHEN is_del IS NULL OR is_del = 0 THEN 0 ELSE 1 END ASC,
42
+ CASE WHEN is_actual IS NULL THEN 0 ELSE is_actual END DESC,
43
+ updated_at DESC,
44
+ created_at DESC,
45
+ id DESC
46
+ `, userId ? [userId] : []);
47
+ const nodes = [];
48
+ const seenPaths = new Set();
49
+ for (const row of rows) {
50
+ const id = trimString(row.id);
51
+ const fullPath = canonicalizeTagPath(row.full_path ?? row.path_key ?? '');
52
+ if (!id || !fullPath) {
53
+ continue;
54
+ }
55
+ const key = normalizeTagLookupKey(fullPath);
56
+ if (options.dedupeByPath && seenPaths.has(key)) {
57
+ continue;
58
+ }
59
+ seenPaths.add(key);
60
+ nodes.push({
61
+ id,
62
+ fullPath,
63
+ pathKey: canonicalizeTagPath(row.path_key ?? fullPath) || fullPath,
64
+ parentId: trimString(row.parent_id) || null,
65
+ depth: typeof row.depth === 'number' ? row.depth : null,
66
+ isActual: typeof row.is_actual === 'number' ? row.is_actual : null,
67
+ isDel: typeof row.is_del === 'number' ? row.is_del : null,
68
+ userId: trimString(row.user_id),
69
+ emoji: trimString(row.emoji),
70
+ });
71
+ }
72
+ return nodes;
73
+ }
74
+ export function activeTagNodes(nodes) {
75
+ return nodes.filter(isActiveTagNode);
76
+ }
77
+ export function actualActiveTagNodes(nodes) {
78
+ return nodes.filter((node) => isActiveTagNode(node) && isActualTagNode(node));
79
+ }
80
+ export function tagNodeByPath(nodes, options = {}) {
81
+ const map = new Map();
82
+ const candidates = options.actualOnly ? actualActiveTagNodes(nodes) : activeTagNodes(nodes);
83
+ for (const node of candidates) {
84
+ const key = normalizeTagLookupKey(node.fullPath);
85
+ if (!map.has(key)) {
86
+ map.set(key, node);
87
+ }
88
+ }
89
+ return map;
90
+ }
91
+ export function requireActiveTagPath(nodes, rawPath, label) {
92
+ const path = canonicalizeTagPath(rawPath);
93
+ if (!path) {
94
+ throw new DinoxError(`${label} is required`);
95
+ }
96
+ const node = tagNodeByPath(nodes).get(normalizeTagLookupKey(path));
97
+ if (!node) {
98
+ throw new DinoxError(`Unknown tag path: ${path}`);
99
+ }
100
+ return node;
101
+ }
102
+ export function descendantsOf(nodes, root, includeRoot = true) {
103
+ const rootKey = `${normalizeTagLookupKey(root.fullPath)}/`;
104
+ return activeTagNodes(nodes)
105
+ .filter((node) => {
106
+ if (includeRoot && node.id === root.id) {
107
+ return true;
108
+ }
109
+ return normalizeTagLookupKey(node.fullPath).startsWith(rootKey);
110
+ })
111
+ .sort((left, right) => left.fullPath.localeCompare(right.fullPath));
112
+ }
113
+ export function toTagListItem(node) {
114
+ return {
115
+ id: node.id,
116
+ tag: node.fullPath,
117
+ name: getLeafName(node.fullPath),
118
+ parentId: node.parentId,
119
+ depth: node.depth,
120
+ emoji: node.emoji,
121
+ };
122
+ }
123
+ export function assertWritableTagNodes(nodes, ids, userId) {
124
+ const foreign = nodes
125
+ .filter((node) => ids.has(node.id))
126
+ .filter((node) => node.userId && node.userId !== userId);
127
+ if (foreign.length > 0) {
128
+ throw new DinoxError(`Tags belong to a different user: ${foreign.map((node) => node.fullPath).join(', ')}`);
129
+ }
130
+ }
131
+ export function ensureNoTagPathConflict(nodes, changes) {
132
+ const changedIds = new Set(changes.map((change) => change.id));
133
+ const activePaths = new Map();
134
+ for (const node of activeTagNodes(nodes)) {
135
+ if (changedIds.has(node.id)) {
136
+ continue;
137
+ }
138
+ activePaths.set(normalizeTagLookupKey(node.fullPath), node);
139
+ }
140
+ const seen = new Set();
141
+ for (const change of changes) {
142
+ const key = normalizeTagLookupKey(change.toPath);
143
+ const existing = activePaths.get(key);
144
+ if (existing) {
145
+ throw new DinoxError(`Target tag path already exists: ${change.toPath}`, {
146
+ details: { type: 'tag_path_conflict', path: change.toPath, existingId: existing.id },
147
+ });
148
+ }
149
+ if (seen.has(key)) {
150
+ throw new DinoxError(`Duplicate target tag path in operation: ${change.toPath}`);
151
+ }
152
+ seen.add(key);
153
+ }
154
+ }
155
+ export function parentIdForPath(path, nodes, changes) {
156
+ const parentPath = parentPathOf(path);
157
+ if (!parentPath) {
158
+ return null;
159
+ }
160
+ const key = normalizeTagLookupKey(parentPath);
161
+ const changedParent = changes.find((change) => normalizeTagLookupKey(change.toPath) === key);
162
+ if (changedParent) {
163
+ return changedParent.id;
164
+ }
165
+ return tagNodeByPath(nodes).get(key)?.id ?? null;
166
+ }