@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.
- package/README.md +756 -472
- package/dist/auth/userInfo.d.ts +10 -1
- package/dist/auth/userInfo.js +66 -17
- package/dist/cli.js +0 -2
- package/dist/commands/auth/index.js +29 -14
- package/dist/commands/boxes/index.js +272 -52
- package/dist/commands/boxes/model.d.ts +34 -0
- package/dist/commands/boxes/model.js +78 -0
- package/dist/commands/boxes/operations.d.ts +86 -0
- package/dist/commands/boxes/operations.js +415 -0
- package/dist/commands/boxes/repo.d.ts +13 -6
- package/dist/commands/boxes/repo.js +272 -149
- package/dist/commands/boxes/store.d.ts +22 -0
- package/dist/commands/boxes/store.js +143 -0
- package/dist/commands/graph/index.js +2 -36
- package/dist/commands/mcp/noteContentTools.d.ts +222 -0
- package/dist/commands/mcp/noteContentTools.js +390 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +45 -16
- package/dist/commands/mcp/tools.js +3 -1
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/meta/index.js +6 -22
- package/dist/commands/meta/repo.d.ts +0 -3
- package/dist/commands/meta/repo.js +1 -4
- package/dist/commands/notes/actions.d.ts +2 -0
- package/dist/commands/notes/actions.js +149 -55
- package/dist/commands/notes/bulk.d.ts +34 -0
- package/dist/commands/notes/bulk.js +204 -0
- package/dist/commands/notes/contentEngine.d.ts +95 -0
- package/dist/commands/notes/contentEngine.js +652 -0
- package/dist/commands/notes/contentPatch.d.ts +84 -0
- package/dist/commands/notes/contentPatch.js +337 -0
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +126 -0
- package/dist/commands/notes/descriptors.js +192 -40
- package/dist/commands/notes/export.d.ts +21 -0
- package/dist/commands/notes/export.js +224 -0
- package/dist/commands/notes/metadataMutation.d.ts +28 -0
- package/dist/commands/notes/metadataMutation.js +127 -0
- package/dist/commands/notes/repo.d.ts +20 -0
- package/dist/commands/notes/repo.js +135 -18
- package/dist/commands/notes/runtime.d.ts +3 -12
- package/dist/commands/notes/runtime.js +4 -30
- package/dist/commands/notes/searchSql.js +1 -1
- package/dist/commands/prompt/index.js +15 -45
- package/dist/commands/prompt/repo.js +2 -2
- package/dist/commands/runtime.d.ts +26 -0
- package/dist/commands/runtime.js +76 -0
- package/dist/commands/schema/renderSkills.js +20 -7
- package/dist/commands/schema/spec.d.ts +1 -2
- package/dist/commands/schema/spec.js +2 -3
- package/dist/commands/schema/types.d.ts +0 -13
- package/dist/commands/storage/index.js +30 -60
- package/dist/commands/storage/repo.d.ts +1 -1
- package/dist/commands/storage/repo.js +20 -1
- package/dist/commands/sync.js +4 -2
- package/dist/commands/tags/content.d.ts +32 -0
- package/dist/commands/tags/content.js +243 -0
- package/dist/commands/tags/index.js +285 -49
- package/dist/commands/tags/model.d.ts +38 -0
- package/dist/commands/tags/model.js +100 -0
- package/dist/commands/tags/operations.d.ts +105 -0
- package/dist/commands/tags/operations.js +438 -0
- package/dist/commands/tags/repo.d.ts +5 -1
- package/dist/commands/tags/repo.js +142 -218
- package/dist/commands/tags/store.d.ts +28 -0
- package/dist/commands/tags/store.js +166 -0
- package/dist/commands/todo/index.js +30 -61
- package/dist/commands/writeDryRun.d.ts +25 -0
- package/dist/commands/writeDryRun.js +31 -1
- package/dist/config/paths.d.ts +2 -1
- package/dist/config/paths.js +11 -1
- package/dist/config/resolve.js +9 -7
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +2 -1
- package/dist/powersync/runtime.js +15 -3
- package/dist/powersync/schema/content.d.ts +2 -0
- package/dist/powersync/schema/content.js +2 -0
- package/dist/powersync/schema/index.d.ts +2 -0
- package/dist/skills/check.d.ts +1 -1
- package/dist/skills/check.js +27 -2
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +7 -2
- package/dist/utils/noteContentEngine.d.ts +333 -0
- package/dist/utils/noteContentEngine.js +740 -0
- package/dist/utils/tiptapMarkdownSchema.js +31 -0
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
181
|
-
existing.isDel = 0;
|
|
83
|
+
setValue('is_del', 0);
|
|
182
84
|
}
|
|
183
85
|
if (isLeaf && existing.isActual !== 1) {
|
|
184
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|