@dinoxx/dinox-cli 1.0.20 → 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 +72 -139
- package/dist/auth/userInfo.d.ts +8 -1
- package/dist/auth/userInfo.js +38 -6
- package/dist/cli.js +0 -2
- package/dist/commands/auth/index.js +11 -4
- 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 +43 -15
- 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/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/resolve.js +2 -2
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +1 -0
- package/dist/powersync/runtime.js +10 -0
- 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,20 +1,40 @@
|
|
|
1
1
|
import { generateId } from '../../utils/id.js';
|
|
2
2
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
4
|
+
import { MAX_BOX_COLOR_LENGTH, MAX_BOX_DESCRIPTION_LENGTH, MAX_BOX_NAME_LENGTH, MAX_BOX_PATH_LENGTH, assertMaxLength, canonicalizeBoxPath, getLeafName, normalizeBoxLookupKey, toNullableId, trimString, } from './model.js';
|
|
5
|
+
import { assertCanWriteBox, getTableColumns } from './store.js';
|
|
6
|
+
export { canonicalizeBoxPath } from './model.js';
|
|
7
|
+
function normalizeLookupKey(value) {
|
|
8
|
+
return normalizeBoxLookupKey(value);
|
|
5
9
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
function parseAddBoxInput(input) {
|
|
11
|
+
const userId = input.userId.trim();
|
|
12
|
+
const path = canonicalizeBoxPath(input.name);
|
|
13
|
+
if (!path) {
|
|
14
|
+
throw new DinoxError('Box path is required');
|
|
15
|
+
}
|
|
16
|
+
const name = getLeafName(path);
|
|
17
|
+
const description = trimString(input.description);
|
|
18
|
+
const color = trimString(input.color);
|
|
19
|
+
assertMaxLength('Box name', name, MAX_BOX_NAME_LENGTH);
|
|
20
|
+
assertMaxLength('Box path', path, MAX_BOX_PATH_LENGTH);
|
|
21
|
+
assertMaxLength('Box description', description, MAX_BOX_DESCRIPTION_LENGTH);
|
|
22
|
+
assertMaxLength('Box color', color, MAX_BOX_COLOR_LENGTH);
|
|
23
|
+
return {
|
|
24
|
+
userId,
|
|
25
|
+
path,
|
|
26
|
+
name,
|
|
27
|
+
description,
|
|
28
|
+
color,
|
|
29
|
+
};
|
|
12
30
|
}
|
|
13
|
-
async function
|
|
14
|
-
|
|
31
|
+
async function loadBoxNodesByPath(db) {
|
|
32
|
+
const rows = await db.getAll(`
|
|
15
33
|
SELECT
|
|
16
34
|
TRIM(COALESCE(id, '')) AS id,
|
|
17
35
|
TRIM(COALESCE(name, '')) AS name,
|
|
36
|
+
TRIM(COALESCE(path, name, '')) AS path,
|
|
37
|
+
TRIM(COALESCE(parent_id, '')) AS parent_id,
|
|
18
38
|
TRIM(COALESCE(description, '')) AS description,
|
|
19
39
|
TRIM(COALESCE(color, '')) AS color,
|
|
20
40
|
is_del,
|
|
@@ -27,169 +47,272 @@ async function listBoxRowsForWrite(db) {
|
|
|
27
47
|
created_at DESC,
|
|
28
48
|
id DESC
|
|
29
49
|
`, []);
|
|
50
|
+
const nodesByPath = new Map();
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const id = trimString(row.id);
|
|
53
|
+
if (!id) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
|
|
57
|
+
if (!path) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const key = normalizeLookupKey(path);
|
|
61
|
+
if (nodesByPath.has(key)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
nodesByPath.set(key, {
|
|
65
|
+
id,
|
|
66
|
+
name: trimString(row.name) || getLeafName(path),
|
|
67
|
+
path,
|
|
68
|
+
parentId: toNullableId(row.parent_id),
|
|
69
|
+
description: trimString(row.description),
|
|
70
|
+
color: trimString(row.color),
|
|
71
|
+
isDel: typeof row.is_del === 'number' ? row.is_del : null,
|
|
72
|
+
userNo: trimString(row.user_no),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return nodesByPath;
|
|
30
76
|
}
|
|
31
|
-
export async function listBoxes(db) {
|
|
77
|
+
export async function listBoxes(db, input = {}) {
|
|
78
|
+
const userId = trimString(input.userId);
|
|
79
|
+
const userFilter = userId
|
|
80
|
+
? `AND (TRIM(COALESCE(user_no, '')) = ? OR TRIM(COALESCE(user_no, '')) = '')`
|
|
81
|
+
: '';
|
|
32
82
|
const rows = await db.getAll(`
|
|
33
83
|
SELECT DISTINCT
|
|
34
84
|
TRIM(COALESCE(id, '')) AS id,
|
|
35
85
|
TRIM(COALESCE(name, '')) AS name,
|
|
36
|
-
TRIM(COALESCE(
|
|
86
|
+
TRIM(COALESCE(path, name, '')) AS path,
|
|
87
|
+
TRIM(COALESCE(parent_id, '')) AS parent_id,
|
|
88
|
+
TRIM(COALESCE(description, '')) AS description,
|
|
89
|
+
TRIM(COALESCE(color, '')) AS color
|
|
37
90
|
FROM c_zettel_box
|
|
38
91
|
WHERE (is_del IS NULL OR is_del = 0)
|
|
39
92
|
AND TRIM(COALESCE(id, '')) <> ''
|
|
40
|
-
|
|
41
|
-
|
|
93
|
+
${userFilter}
|
|
94
|
+
ORDER BY path COLLATE NOCASE ASC, id ASC
|
|
95
|
+
`, userId ? [userId] : []);
|
|
42
96
|
return rows
|
|
43
|
-
.map((row) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
.map((row) => {
|
|
98
|
+
const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
|
|
99
|
+
return {
|
|
100
|
+
id: trimString(row.id),
|
|
101
|
+
name: trimString(row.name) || (path ? getLeafName(path) : ''),
|
|
102
|
+
path,
|
|
103
|
+
parentId: toNullableId(row.parent_id),
|
|
104
|
+
description: trimString(row.description),
|
|
105
|
+
color: trimString(row.color),
|
|
106
|
+
};
|
|
107
|
+
})
|
|
108
|
+
.filter((row) => row.id.length > 0 && row.path.length > 0);
|
|
49
109
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
function buildBoxExistsError(path, ids) {
|
|
111
|
+
return new DinoxError(`Zettel box already exists: ${path}`, {
|
|
112
|
+
details: {
|
|
113
|
+
type: 'box_exists',
|
|
114
|
+
name: getLeafName(path),
|
|
115
|
+
path,
|
|
116
|
+
ids,
|
|
117
|
+
question: 'This box already exists. Do you want to keep using the existing box?',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function setExistingValue(assignments, params, node, field, column, value) {
|
|
122
|
+
if (node[field] === value) {
|
|
123
|
+
return;
|
|
55
124
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
assignments.push(`${column} = ?`);
|
|
126
|
+
params.push(value);
|
|
127
|
+
node[field] = value;
|
|
128
|
+
}
|
|
129
|
+
async function restoreOrUpdateExistingBox(args) {
|
|
130
|
+
const { db, node, name, path, parentId, description, color, userId, timestamp, isLeaf } = args;
|
|
131
|
+
const assignments = [];
|
|
132
|
+
const params = [];
|
|
133
|
+
const wasDeleted = node.isDel === 1;
|
|
134
|
+
if (wasDeleted) {
|
|
135
|
+
assignments.push('is_del = ?');
|
|
136
|
+
params.push(0);
|
|
137
|
+
node.isDel = 0;
|
|
64
138
|
}
|
|
65
|
-
|
|
66
|
-
|
|
139
|
+
setExistingValue(assignments, params, node, 'name', 'name', name);
|
|
140
|
+
setExistingValue(assignments, params, node, 'path', 'path', path);
|
|
141
|
+
setExistingValue(assignments, params, node, 'parentId', 'parent_id', parentId);
|
|
142
|
+
if (!node.userNo) {
|
|
143
|
+
setExistingValue(assignments, params, node, 'userNo', 'user_no', userId);
|
|
67
144
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
if (isLeaf && description) {
|
|
146
|
+
setExistingValue(assignments, params, node, 'description', 'description', description);
|
|
147
|
+
}
|
|
148
|
+
if (isLeaf && color) {
|
|
149
|
+
setExistingValue(assignments, params, node, 'color', 'color', color);
|
|
150
|
+
}
|
|
151
|
+
if (assignments.length === 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
assignments.unshift('updated_at = ?');
|
|
155
|
+
params.unshift(timestamp);
|
|
156
|
+
params.push(node.id);
|
|
157
|
+
await db.execute(`
|
|
158
|
+
UPDATE c_zettel_box
|
|
159
|
+
SET ${assignments.join(', ')}
|
|
160
|
+
WHERE id = ?
|
|
161
|
+
`, params);
|
|
162
|
+
return wasDeleted;
|
|
163
|
+
}
|
|
164
|
+
async function insertBoxNode(args) {
|
|
165
|
+
const { db, id, name, path, parentId, description, color, userId, timestamp } = args;
|
|
166
|
+
await db.execute(`
|
|
167
|
+
INSERT INTO c_zettel_box (
|
|
168
|
+
id,
|
|
169
|
+
name,
|
|
170
|
+
path,
|
|
171
|
+
parent_id,
|
|
172
|
+
description,
|
|
173
|
+
color,
|
|
174
|
+
priority,
|
|
175
|
+
is_show,
|
|
176
|
+
cover_url,
|
|
177
|
+
created_at,
|
|
178
|
+
updated_at,
|
|
179
|
+
extra_data,
|
|
180
|
+
user_no,
|
|
181
|
+
is_del
|
|
182
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, 1, '', ?, ?, '{}', ?, 0)
|
|
183
|
+
`, [id, name, path, parentId, description, color, timestamp, timestamp, userId]);
|
|
184
|
+
}
|
|
185
|
+
export async function addBox(db, input) {
|
|
186
|
+
const parsed = parseAddBoxInput(input);
|
|
187
|
+
const columnNames = await getTableColumns(db, 'c_zettel_box');
|
|
188
|
+
assertCanWriteBox(columnNames, parsed.userId, 'insert');
|
|
189
|
+
const nodesByPath = await loadBoxNodesByPath(db);
|
|
190
|
+
const existingLeaf = nodesByPath.get(normalizeLookupKey(parsed.path));
|
|
191
|
+
if (existingLeaf && existingLeaf.isDel !== 1) {
|
|
192
|
+
throw buildBoxExistsError(parsed.path, [existingLeaf.id]);
|
|
81
193
|
}
|
|
82
194
|
const timestamp = new Date().toISOString();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
195
|
+
const segments = parsed.path.split('/');
|
|
196
|
+
let parentId = null;
|
|
197
|
+
let leafId = '';
|
|
198
|
+
let leafParentId = null;
|
|
199
|
+
let leafDescription = '';
|
|
200
|
+
let leafColor = '';
|
|
201
|
+
let createdNodes = 0;
|
|
202
|
+
let restoredNodes = 0;
|
|
203
|
+
let restoredLeaf = false;
|
|
204
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
205
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
206
|
+
const path = segments.slice(0, index + 1).join('/');
|
|
207
|
+
const key = normalizeLookupKey(path);
|
|
208
|
+
const name = segments[index];
|
|
209
|
+
const isLeaf = index === segments.length - 1;
|
|
210
|
+
const existing = nodesByPath.get(key);
|
|
211
|
+
if (existing) {
|
|
212
|
+
const restored = await restoreOrUpdateExistingBox({
|
|
213
|
+
db: tx,
|
|
214
|
+
node: existing,
|
|
215
|
+
name,
|
|
216
|
+
path,
|
|
217
|
+
parentId,
|
|
218
|
+
description: parsed.description,
|
|
219
|
+
color: parsed.color,
|
|
220
|
+
userId: parsed.userId,
|
|
221
|
+
timestamp,
|
|
222
|
+
isLeaf,
|
|
223
|
+
});
|
|
224
|
+
if (restored) {
|
|
225
|
+
restoredNodes += 1;
|
|
226
|
+
restoredLeaf = isLeaf;
|
|
227
|
+
}
|
|
228
|
+
parentId = existing.id;
|
|
229
|
+
if (isLeaf) {
|
|
230
|
+
leafId = existing.id;
|
|
231
|
+
leafParentId = existing.parentId;
|
|
232
|
+
leafDescription = existing.description;
|
|
233
|
+
leafColor = existing.color;
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const id = generateId();
|
|
238
|
+
await insertBoxNode({
|
|
239
|
+
db: tx,
|
|
240
|
+
id,
|
|
241
|
+
name,
|
|
242
|
+
path,
|
|
243
|
+
parentId,
|
|
244
|
+
description: isLeaf && parsed.description ? parsed.description : null,
|
|
245
|
+
color: isLeaf && parsed.color ? parsed.color : null,
|
|
246
|
+
userId: parsed.userId,
|
|
247
|
+
timestamp,
|
|
248
|
+
});
|
|
249
|
+
const node = {
|
|
250
|
+
id,
|
|
251
|
+
name,
|
|
252
|
+
path,
|
|
253
|
+
parentId,
|
|
254
|
+
description: isLeaf ? parsed.description : '',
|
|
255
|
+
color: isLeaf ? parsed.color : '',
|
|
256
|
+
isDel: 0,
|
|
257
|
+
userNo: parsed.userId,
|
|
258
|
+
};
|
|
259
|
+
nodesByPath.set(key, node);
|
|
260
|
+
createdNodes += 1;
|
|
261
|
+
parentId = id;
|
|
262
|
+
if (isLeaf) {
|
|
263
|
+
leafId = id;
|
|
264
|
+
leafParentId = node.parentId;
|
|
265
|
+
leafDescription = node.description;
|
|
266
|
+
leafColor = node.color;
|
|
90
267
|
}
|
|
91
|
-
setParts.push(`${column} = ?`);
|
|
92
|
-
params.push(value);
|
|
93
|
-
};
|
|
94
|
-
setValue('name', name);
|
|
95
|
-
setValue('is_del', 0);
|
|
96
|
-
setValue('updated_at', timestamp);
|
|
97
|
-
if (description) {
|
|
98
|
-
setValue('description', description);
|
|
99
|
-
}
|
|
100
|
-
if (color) {
|
|
101
|
-
setValue('color', color);
|
|
102
|
-
}
|
|
103
|
-
setValue('user_no', userId);
|
|
104
|
-
if (setParts.length > 0) {
|
|
105
|
-
params.push(deletedRow.id);
|
|
106
|
-
await db.execute(`
|
|
107
|
-
UPDATE c_zettel_box
|
|
108
|
-
SET ${setParts.join(', ')}
|
|
109
|
-
WHERE id = ?
|
|
110
|
-
`, params);
|
|
111
268
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
description: description || (deletedRow.description?.trim() ?? ''),
|
|
116
|
-
color: color || (deletedRow.color?.trim() ?? ''),
|
|
117
|
-
restored: true,
|
|
118
|
-
};
|
|
269
|
+
});
|
|
270
|
+
if (!leafId) {
|
|
271
|
+
throw new DinoxError(`Failed to create zettel box: ${parsed.path}`);
|
|
119
272
|
}
|
|
120
|
-
const id = generateId();
|
|
121
|
-
const insertEntries = [];
|
|
122
|
-
const pushInsert = (column, value) => {
|
|
123
|
-
if (!columnNames.has(column)) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
insertEntries.push([column, value]);
|
|
127
|
-
};
|
|
128
|
-
pushInsert('id', id);
|
|
129
|
-
pushInsert('name', name);
|
|
130
|
-
pushInsert('description', description || null);
|
|
131
|
-
pushInsert('color', color || null);
|
|
132
|
-
pushInsert('priority', 0);
|
|
133
|
-
pushInsert('is_show', 1);
|
|
134
|
-
pushInsert('cover_url', '');
|
|
135
|
-
pushInsert('created_at', timestamp);
|
|
136
|
-
pushInsert('updated_at', timestamp);
|
|
137
|
-
pushInsert('extra_data', '{}');
|
|
138
|
-
pushInsert('user_no', userId);
|
|
139
|
-
pushInsert('is_del', 0);
|
|
140
|
-
const insertColumns = insertEntries.map(([column]) => column);
|
|
141
|
-
const placeholders = insertColumns.map(() => '?').join(', ');
|
|
142
|
-
const insertParams = insertEntries.map(([, value]) => value);
|
|
143
|
-
await db.execute(`
|
|
144
|
-
INSERT INTO c_zettel_box (${insertColumns.join(', ')})
|
|
145
|
-
VALUES (${placeholders})
|
|
146
|
-
`, insertParams);
|
|
147
273
|
return {
|
|
148
|
-
id,
|
|
149
|
-
name,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
274
|
+
id: leafId,
|
|
275
|
+
name: parsed.name,
|
|
276
|
+
path: parsed.path,
|
|
277
|
+
parentId: leafParentId,
|
|
278
|
+
description: leafDescription,
|
|
279
|
+
color: leafColor,
|
|
280
|
+
restored: restoredLeaf,
|
|
281
|
+
createdNodes,
|
|
282
|
+
restoredNodes,
|
|
153
283
|
};
|
|
154
284
|
}
|
|
155
285
|
export async function inspectAddBox(db, input) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const lookup = normalizeLookup(name);
|
|
164
|
-
const columnNames = await getBoxColumns(db);
|
|
165
|
-
const requiredColumns = ['id', 'name', 'user_no'];
|
|
166
|
-
const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
|
|
167
|
-
if (missingRequired.length > 0) {
|
|
168
|
-
throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
|
|
169
|
-
}
|
|
170
|
-
if (!userId) {
|
|
171
|
-
throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
|
|
286
|
+
const parsed = parseAddBoxInput(input);
|
|
287
|
+
const columnNames = await getTableColumns(db, 'c_zettel_box');
|
|
288
|
+
assertCanWriteBox(columnNames, parsed.userId, 'insert');
|
|
289
|
+
const nodesByPath = await loadBoxNodesByPath(db);
|
|
290
|
+
const existingLeaf = nodesByPath.get(normalizeLookupKey(parsed.path));
|
|
291
|
+
if (existingLeaf && existingLeaf.isDel !== 1) {
|
|
292
|
+
throw buildBoxExistsError(parsed.path, [existingLeaf.id]);
|
|
172
293
|
}
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
294
|
+
const segments = parsed.path.split('/');
|
|
295
|
+
const createdPaths = [];
|
|
296
|
+
const restoredPaths = [];
|
|
297
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
298
|
+
const path = segments.slice(0, index + 1).join('/');
|
|
299
|
+
const existing = nodesByPath.get(normalizeLookupKey(path));
|
|
300
|
+
if (!existing) {
|
|
301
|
+
createdPaths.push(path);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (existing.isDel === 1) {
|
|
305
|
+
restoredPaths.push(path);
|
|
306
|
+
}
|
|
186
307
|
}
|
|
187
|
-
const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
|
|
188
308
|
return {
|
|
189
|
-
name,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
309
|
+
name: parsed.name,
|
|
310
|
+
path: parsed.path,
|
|
311
|
+
description: parsed.description,
|
|
312
|
+
color: parsed.color,
|
|
313
|
+
restored: existingLeaf?.isDel === 1,
|
|
314
|
+
createdPaths,
|
|
315
|
+
restoredPaths,
|
|
316
|
+
...(existingLeaf?.id ? { existingId: existingLeaf.id } : {}),
|
|
194
317
|
};
|
|
195
318
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import type { BoxListItem, MutableBoxNode } from './model.js';
|
|
3
|
+
type BoxTableName = 'c_zettel_box' | 'c_note';
|
|
4
|
+
export declare function getTableColumns(db: AbstractPowerSyncDatabase, table: BoxTableName): Promise<Set<string>>;
|
|
5
|
+
export declare function assertRequiredColumns(table: string, columnNames: Set<string>, required: string[]): void;
|
|
6
|
+
export declare function assertCanWriteBox(columnNames: Set<string>, userId: string, action: 'insert' | 'update'): void;
|
|
7
|
+
export declare function loadAllBoxNodes(db: AbstractPowerSyncDatabase, options?: {
|
|
8
|
+
userId?: string;
|
|
9
|
+
}): Promise<MutableBoxNode[]>;
|
|
10
|
+
export declare function isActiveBoxNode(node: MutableBoxNode): boolean;
|
|
11
|
+
export declare function toBoxListItem(node: MutableBoxNode): BoxListItem;
|
|
12
|
+
export declare function activeBoxNodes(nodes: MutableBoxNode[]): MutableBoxNode[];
|
|
13
|
+
export declare function activeNodeByPath(nodes: MutableBoxNode[]): Map<string, MutableBoxNode>;
|
|
14
|
+
export declare function requireActivePath(nodes: MutableBoxNode[], rawPath: string, label: string): MutableBoxNode;
|
|
15
|
+
export declare function isDescendantBoxPath(candidatePath: string, rootPath: string): boolean;
|
|
16
|
+
export declare function descendantsOf(nodes: MutableBoxNode[], root: MutableBoxNode, includeRoot?: boolean): MutableBoxNode[];
|
|
17
|
+
export declare function ensureNoPathConflict(nodes: MutableBoxNode[], changes: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
toPath: string;
|
|
20
|
+
}>): void;
|
|
21
|
+
export declare function assertWritableBoxNodes(nodes: MutableBoxNode[], ids: Set<string>, userId: string): void;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
2
|
+
import { REQUIRED_ZETTEL_BOX_COLUMNS, canonicalizeBoxPath, getLeafName, normalizeBoxLookupKey, toNullableId, trimString, } from './model.js';
|
|
3
|
+
export async function getTableColumns(db, table) {
|
|
4
|
+
const sql = table === 'c_zettel_box' ? 'PRAGMA table_info(c_zettel_box)' : '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 assertCanWriteBox(columnNames, userId, action) {
|
|
15
|
+
assertRequiredColumns('c_zettel_box', columnNames, REQUIRED_ZETTEL_BOX_COLUMNS);
|
|
16
|
+
if (!userId.trim()) {
|
|
17
|
+
throw new DinoxError(`c_zettel_box ${action} requires user_no. 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 loadAllBoxNodes(db, options = {}) {
|
|
21
|
+
const userId = trimString(options.userId);
|
|
22
|
+
const userFilter = userId
|
|
23
|
+
? `AND (TRIM(COALESCE(user_no, '')) = ? OR TRIM(COALESCE(user_no, '')) = '')`
|
|
24
|
+
: '';
|
|
25
|
+
const rows = await db.getAll(`
|
|
26
|
+
SELECT
|
|
27
|
+
TRIM(COALESCE(id, '')) AS id,
|
|
28
|
+
TRIM(COALESCE(name, '')) AS name,
|
|
29
|
+
TRIM(COALESCE(path, name, '')) AS path,
|
|
30
|
+
TRIM(COALESCE(parent_id, '')) AS parent_id,
|
|
31
|
+
TRIM(COALESCE(description, '')) AS description,
|
|
32
|
+
TRIM(COALESCE(color, '')) AS color,
|
|
33
|
+
is_del,
|
|
34
|
+
TRIM(COALESCE(user_no, '')) AS user_no
|
|
35
|
+
FROM c_zettel_box
|
|
36
|
+
WHERE TRIM(COALESCE(id, '')) <> ''
|
|
37
|
+
${userFilter}
|
|
38
|
+
ORDER BY path COLLATE NOCASE ASC, id ASC
|
|
39
|
+
`, userId ? [userId] : []);
|
|
40
|
+
return rows
|
|
41
|
+
.map((row) => {
|
|
42
|
+
const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
|
|
43
|
+
return {
|
|
44
|
+
id: trimString(row.id),
|
|
45
|
+
name: trimString(row.name) || (path ? getLeafName(path) : ''),
|
|
46
|
+
path,
|
|
47
|
+
parentId: toNullableId(row.parent_id),
|
|
48
|
+
description: trimString(row.description),
|
|
49
|
+
color: trimString(row.color),
|
|
50
|
+
isDel: typeof row.is_del === 'number' ? row.is_del : null,
|
|
51
|
+
userNo: trimString(row.user_no),
|
|
52
|
+
};
|
|
53
|
+
})
|
|
54
|
+
.filter((row) => row.id && row.path);
|
|
55
|
+
}
|
|
56
|
+
export function isActiveBoxNode(node) {
|
|
57
|
+
return node.isDel === null || node.isDel === 0;
|
|
58
|
+
}
|
|
59
|
+
export function toBoxListItem(node) {
|
|
60
|
+
return {
|
|
61
|
+
id: node.id,
|
|
62
|
+
name: node.name,
|
|
63
|
+
path: node.path,
|
|
64
|
+
parentId: node.parentId,
|
|
65
|
+
description: node.description,
|
|
66
|
+
color: node.color,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function activeBoxNodes(nodes) {
|
|
70
|
+
return nodes.filter(isActiveBoxNode);
|
|
71
|
+
}
|
|
72
|
+
export function activeNodeByPath(nodes) {
|
|
73
|
+
const map = new Map();
|
|
74
|
+
for (const node of activeBoxNodes(nodes)) {
|
|
75
|
+
const key = normalizeBoxLookupKey(node.path);
|
|
76
|
+
if (!map.has(key)) {
|
|
77
|
+
map.set(key, node);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return map;
|
|
81
|
+
}
|
|
82
|
+
export function requireActivePath(nodes, rawPath, label) {
|
|
83
|
+
const path = canonicalizeBoxPath(rawPath);
|
|
84
|
+
if (!path) {
|
|
85
|
+
throw new DinoxError(`${label} is required`);
|
|
86
|
+
}
|
|
87
|
+
const node = activeNodeByPath(nodes).get(normalizeBoxLookupKey(path));
|
|
88
|
+
if (!node) {
|
|
89
|
+
throw new DinoxError(`Unknown zettel box path: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
export function isDescendantBoxPath(candidatePath, rootPath) {
|
|
94
|
+
const candidateKey = normalizeBoxLookupKey(candidatePath);
|
|
95
|
+
const rootKey = normalizeBoxLookupKey(rootPath);
|
|
96
|
+
return candidateKey.startsWith(`${rootKey}/`);
|
|
97
|
+
}
|
|
98
|
+
export function descendantsOf(nodes, root, includeRoot = true) {
|
|
99
|
+
return activeBoxNodes(nodes)
|
|
100
|
+
.filter((node) => {
|
|
101
|
+
if (includeRoot && node.id === root.id) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return isDescendantBoxPath(node.path, root.path);
|
|
105
|
+
})
|
|
106
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
107
|
+
}
|
|
108
|
+
export function ensureNoPathConflict(nodes, changes) {
|
|
109
|
+
const changedIds = new Set(changes.map((change) => change.id));
|
|
110
|
+
const activePaths = new Map();
|
|
111
|
+
for (const node of activeBoxNodes(nodes)) {
|
|
112
|
+
if (changedIds.has(node.id)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
activePaths.set(normalizeBoxLookupKey(node.path), node);
|
|
116
|
+
}
|
|
117
|
+
const seenNext = new Set();
|
|
118
|
+
for (const change of changes) {
|
|
119
|
+
const key = normalizeBoxLookupKey(change.toPath);
|
|
120
|
+
const existing = activePaths.get(key);
|
|
121
|
+
if (existing) {
|
|
122
|
+
throw new DinoxError(`Target zettel box path already exists: ${change.toPath}`, {
|
|
123
|
+
details: {
|
|
124
|
+
type: 'box_path_conflict',
|
|
125
|
+
path: change.toPath,
|
|
126
|
+
existingId: existing.id,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (seenNext.has(key)) {
|
|
131
|
+
throw new DinoxError(`Duplicate target zettel box path in operation: ${change.toPath}`);
|
|
132
|
+
}
|
|
133
|
+
seenNext.add(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function assertWritableBoxNodes(nodes, ids, userId) {
|
|
137
|
+
const foreign = nodes
|
|
138
|
+
.filter((node) => ids.has(node.id))
|
|
139
|
+
.filter((node) => node.userNo && node.userNo !== userId);
|
|
140
|
+
if (foreign.length > 0) {
|
|
141
|
+
throw new DinoxError(`Zettel boxes belong to a different user: ${foreign.map((node) => node.path).join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
}
|