@dinoxx/dinox-cli 1.1.0 → 2.0.0
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 +48 -4
- package/dist/cli.js +4 -0
- package/dist/commands/auth/index.js +1 -2
- package/dist/commands/boxes/index.js +37 -13
- package/dist/commands/boxes/operations.d.ts +3 -3
- package/dist/commands/boxes/operations.js +51 -28
- package/dist/commands/info/index.js +30 -5
- package/dist/commands/meta/repo.js +2 -1
- package/dist/commands/notes/actions.js +20 -37
- package/dist/commands/notes/bulk.d.ts +2 -2
- package/dist/commands/notes/bulk.js +3 -18
- package/dist/commands/notes/contentPatch.d.ts +5 -1
- package/dist/commands/notes/contentPatch.js +12 -1
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +29 -8
- package/dist/commands/notes/descriptors.js +19 -16
- package/dist/commands/notes/export.js +3 -0
- package/dist/commands/notes/repo.d.ts +9 -0
- package/dist/commands/notes/repo.js +65 -15
- package/dist/commands/notes/searchOutput.d.ts +2 -1
- package/dist/commands/notes/searchOutput.js +2 -1
- package/dist/commands/notes/searchSql.js +9 -3
- package/dist/commands/prompt/index.d.ts +1 -0
- package/dist/commands/prompt/index.js +26 -11
- package/dist/commands/runtime.js +17 -6
- package/dist/commands/savedViews/index.d.ts +7 -0
- package/dist/commands/savedViews/index.js +458 -0
- package/dist/commands/savedViews/model.d.ts +87 -0
- package/dist/commands/savedViews/model.js +306 -0
- package/dist/commands/savedViews/repo.d.ts +123 -0
- package/dist/commands/savedViews/repo.js +512 -0
- package/dist/commands/savedViews/semantics.d.ts +46 -0
- package/dist/commands/savedViews/semantics.js +738 -0
- package/dist/commands/schema/renderSkills.d.ts +2 -0
- package/dist/commands/schema/renderSkills.js +69 -10
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +54 -0
- package/dist/commands/skills/index.js +468 -0
- package/dist/commands/storage/index.js +2 -2
- package/dist/commands/sync.d.ts +23 -1
- package/dist/commands/sync.js +100 -58
- package/dist/commands/tags/index.js +30 -11
- package/dist/commands/tags/operations.d.ts +3 -3
- package/dist/commands/tags/operations.js +45 -20
- package/dist/commands/templates/model.d.ts +50 -0
- package/dist/commands/templates/model.js +239 -0
- package/dist/commands/todo/index.js +3 -3
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/writeConfirmation.d.ts +13 -0
- package/dist/commands/writeConfirmation.js +54 -0
- package/dist/config/resolve.js +5 -10
- package/dist/config/types.d.ts +3 -6
- package/dist/constants/links.d.ts +18 -1
- package/dist/constants/links.js +26 -1
- package/dist/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +106 -52
- package/dist/daemon/index.d.ts +5 -3
- package/dist/daemon/index.js +28 -5
- package/dist/daemon/lifecycle.d.ts +10 -3
- package/dist/daemon/lifecycle.js +54 -11
- package/dist/daemon/pid.d.ts +1 -1
- package/dist/daemon/pid.js +6 -3
- package/dist/dinox.js +9 -1
- package/dist/powersync/exclusiveRuntime.d.ts +2 -0
- package/dist/powersync/exclusiveRuntime.js +28 -4
- package/dist/powersync/runtime.d.ts +4 -2
- package/dist/powersync/runtime.js +43 -5
- package/dist/powersync/schema/content.d.ts +1 -78
- package/dist/powersync/schema/content.js +1 -78
- package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
- package/dist/powersync/schema/electronCanonical.js +1154 -0
- package/dist/powersync/schema/index.d.ts +573 -129
- package/dist/powersync/schema/index.js +3 -32
- package/dist/powersync/schema/local.d.ts +1 -56
- package/dist/powersync/schema/local.js +1 -60
- package/dist/powersync/schema/note.d.ts +1 -35
- package/dist/powersync/schema/note.js +1 -35
- package/dist/powersync/schema/notesExtras.d.ts +1 -62
- package/dist/powersync/schema/notesExtras.js +1 -71
- package/dist/powersync/schema/projects.d.ts +1 -104
- package/dist/powersync/schema/projects.js +1 -104
- package/dist/powersync/schema/savedViews.d.ts +1 -0
- package/dist/powersync/schema/savedViews.js +1 -0
- package/dist/powersync/schema/storage.d.ts +1 -24
- package/dist/powersync/schema/storage.js +1 -24
- package/dist/powersync/schema/tags.d.ts +1 -37
- package/dist/powersync/schema/tags.js +1 -37
- package/dist/powersync/syncGate.d.ts +22 -3
- package/dist/powersync/syncGate.js +4 -1
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +2 -0
- package/dist/powersync/tokenIndex.js +60 -18
- package/dist/powersync/uploader.d.ts +16 -2
- package/dist/powersync/uploader.js +152 -158
- package/dist/skills/check.d.ts +1 -0
- package/dist/skills/check.js +538 -70
- package/dist/skills/retired.d.ts +60 -0
- package/dist/skills/retired.js +58 -0
- package/dist/utils/deadline.d.ts +8 -0
- package/dist/utils/deadline.js +31 -0
- package/dist/utils/dryRun.d.ts +20 -1
- package/dist/utils/dryRun.js +22 -3
- package/dist/utils/errors.d.ts +2 -0
- package/dist/utils/errors.js +5 -3
- package/dist/utils/processExit.d.ts +1 -0
- package/dist/utils/processExit.js +33 -0
- package/dist/utils/updateNotice.d.ts +1 -0
- package/dist/utils/updateNotice.js +1 -1
- package/package.json +10 -5
package/dist/commands/sync.js
CHANGED
|
@@ -9,13 +9,109 @@ import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
|
9
9
|
import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
|
|
10
10
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
11
11
|
import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
|
|
12
|
+
const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
|
|
13
|
+
const syncCommandDependencies = {
|
|
14
|
+
loadConfig,
|
|
15
|
+
resolveConfig,
|
|
16
|
+
hydrateResolvedAuthIdentity,
|
|
17
|
+
withExclusivePowerSync,
|
|
18
|
+
getRequestedStructuredOutputFormat,
|
|
19
|
+
getStatusSnapshot,
|
|
20
|
+
waitForLastSyncedAfter,
|
|
21
|
+
runSingleSyncPass,
|
|
22
|
+
printHumanReadableLine,
|
|
23
|
+
printSuccess,
|
|
24
|
+
now: Date.now,
|
|
25
|
+
};
|
|
26
|
+
export async function executeSyncCommand(context, overrides = {}) {
|
|
27
|
+
const dependencies = { ...syncCommandDependencies, ...overrides };
|
|
28
|
+
const startedAtMs = dependencies.now();
|
|
29
|
+
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
30
|
+
const baseConfig = dependencies.resolveConfig(await dependencies.loadConfig());
|
|
31
|
+
const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
|
|
32
|
+
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
33
|
+
const deadlineAtMs = startedAtMs + timeoutMs;
|
|
34
|
+
const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
|
|
35
|
+
const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
|
|
36
|
+
const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
|
|
37
|
+
const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
38
|
+
timeoutMs: remainingTimeoutMs(),
|
|
39
|
+
});
|
|
40
|
+
const strict = Boolean(context.options.strict);
|
|
41
|
+
const result = await dependencies.withExclusivePowerSync({
|
|
42
|
+
config,
|
|
43
|
+
offline: false,
|
|
44
|
+
timeoutMs: remainingTimeoutMs(),
|
|
45
|
+
deadlineAtMs,
|
|
46
|
+
operationDeadlineAtMs,
|
|
47
|
+
waitForInitialSync: false,
|
|
48
|
+
expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
|
|
49
|
+
cleanupFailureMode: 'throw',
|
|
50
|
+
}, async ({ db, dbPath, stale }) => {
|
|
51
|
+
const recentSync = strict
|
|
52
|
+
? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
53
|
+
: true;
|
|
54
|
+
const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
|
|
55
|
+
const status = dependencies.getStatusSnapshot(db);
|
|
56
|
+
const gate = evaluateSyncGate({
|
|
57
|
+
offline: false,
|
|
58
|
+
firstSyncStale: stale,
|
|
59
|
+
idle: syncPass.downloadIdle,
|
|
60
|
+
indexComplete: syncPass.tokenIndex.complete,
|
|
61
|
+
recentSync,
|
|
62
|
+
status,
|
|
63
|
+
});
|
|
64
|
+
const payload = {
|
|
65
|
+
ok: true,
|
|
66
|
+
dbPath,
|
|
67
|
+
stale: !gate.ok,
|
|
68
|
+
idle: syncPass.idle,
|
|
69
|
+
downloadIdle: syncPass.downloadIdle,
|
|
70
|
+
uploadIdle: syncPass.uploadIdle,
|
|
71
|
+
strict,
|
|
72
|
+
gate,
|
|
73
|
+
uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
|
|
74
|
+
tokenIndex: syncPass.tokenIndex,
|
|
75
|
+
status,
|
|
76
|
+
};
|
|
77
|
+
if (strict && !gate.ok) {
|
|
78
|
+
throw createSyncRequiredError(gate, status);
|
|
79
|
+
}
|
|
80
|
+
return payload;
|
|
81
|
+
});
|
|
82
|
+
const payload = {
|
|
83
|
+
...result,
|
|
84
|
+
elapsedMs: Math.max(0, dependencies.now() - startedAtMs),
|
|
85
|
+
};
|
|
86
|
+
if (structuredOutput) {
|
|
87
|
+
dependencies.printSuccess(payload);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
dependencies.printHumanReadableLine(`db: ${payload.dbPath}`);
|
|
91
|
+
if (payload.status.lastSyncedAt) {
|
|
92
|
+
dependencies.printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
|
|
93
|
+
}
|
|
94
|
+
if (payload.stale) {
|
|
95
|
+
dependencies.printHumanReadableLine('warning: sync freshness or local index could not be fully verified');
|
|
96
|
+
}
|
|
97
|
+
if (!payload.downloadIdle) {
|
|
98
|
+
dependencies.printHumanReadableLine('warning: download flow did not settle before timeout');
|
|
99
|
+
}
|
|
100
|
+
if (!payload.uploadIdle) {
|
|
101
|
+
dependencies.printHumanReadableLine('warning: uploads are still active; downloaded data may already be current');
|
|
102
|
+
}
|
|
103
|
+
if (!payload.tokenIndex.complete) {
|
|
104
|
+
dependencies.printHumanReadableLine('warning: local token index did not finish before timeout');
|
|
105
|
+
}
|
|
106
|
+
dependencies.printHumanReadableLine(`tokenIndex: complete=${payload.tokenIndex.complete} scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
|
|
107
|
+
}
|
|
12
108
|
export const syncCommandDescriptor = {
|
|
13
109
|
name: 'sync',
|
|
14
110
|
description: 'Connect and synchronize the local PowerSync database',
|
|
15
111
|
options: [
|
|
16
112
|
{
|
|
17
113
|
flags: '--strict',
|
|
18
|
-
description: 'Fail unless connected, a
|
|
114
|
+
description: 'Fail unless connected, a current checkpoint completes, downloads settle, and the local index finishes',
|
|
19
115
|
type: 'boolean',
|
|
20
116
|
},
|
|
21
117
|
],
|
|
@@ -27,65 +123,11 @@ export const syncCommandDescriptor = {
|
|
|
27
123
|
output: {
|
|
28
124
|
type: 'structured',
|
|
29
125
|
summary: 'Sync result with connection status and token index counters',
|
|
30
|
-
fields: ['dbPath', 'stale', 'idle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status'],
|
|
126
|
+
fields: ['dbPath', 'stale', 'idle', 'downloadIdle', 'uploadIdle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status', 'elapsedMs'],
|
|
31
127
|
},
|
|
32
|
-
examples: ['dino sync --format json', 'dino sync --strict --sync-timeout
|
|
33
|
-
},
|
|
34
|
-
action: async (context) => {
|
|
35
|
-
const startedAtIso = new Date().toISOString();
|
|
36
|
-
const baseConfig = resolveConfig(await loadConfig());
|
|
37
|
-
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
38
|
-
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
39
|
-
const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs });
|
|
40
|
-
await withExclusivePowerSync({
|
|
41
|
-
config,
|
|
42
|
-
offline: false,
|
|
43
|
-
timeoutMs,
|
|
44
|
-
expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
|
|
45
|
-
}, async ({ db, dbPath, stale }) => {
|
|
46
|
-
const syncPass = await runSingleSyncPass(db, timeoutMs);
|
|
47
|
-
const recentSync = context.options.strict
|
|
48
|
-
? await waitForLastSyncedAfter(db, startedAtIso, timeoutMs)
|
|
49
|
-
: true;
|
|
50
|
-
const status = getStatusSnapshot(db);
|
|
51
|
-
const gate = evaluateSyncGate({
|
|
52
|
-
offline: false,
|
|
53
|
-
firstSyncStale: stale,
|
|
54
|
-
idle: syncPass.idle,
|
|
55
|
-
recentSync,
|
|
56
|
-
status,
|
|
57
|
-
});
|
|
58
|
-
const payload = {
|
|
59
|
-
ok: true,
|
|
60
|
-
dbPath,
|
|
61
|
-
stale: !gate.ok,
|
|
62
|
-
idle: syncPass.idle,
|
|
63
|
-
strict: Boolean(context.options.strict),
|
|
64
|
-
gate,
|
|
65
|
-
uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
|
|
66
|
-
tokenIndex: syncPass.tokenIndex,
|
|
67
|
-
status,
|
|
68
|
-
};
|
|
69
|
-
if (context.options.strict && !gate.ok) {
|
|
70
|
-
throw createSyncRequiredError(gate, status);
|
|
71
|
-
}
|
|
72
|
-
if (structuredOutput) {
|
|
73
|
-
printSuccess(payload);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
printHumanReadableLine(`db: ${payload.dbPath}`);
|
|
77
|
-
if (payload.status.lastSyncedAt) {
|
|
78
|
-
printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
|
|
79
|
-
}
|
|
80
|
-
if (payload.stale) {
|
|
81
|
-
printHumanReadableLine('warning: sync timed out; local cache may be stale');
|
|
82
|
-
}
|
|
83
|
-
if (!payload.idle) {
|
|
84
|
-
printHumanReadableLine('warning: data flow did not become idle before timeout');
|
|
85
|
-
}
|
|
86
|
-
printHumanReadableLine(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
|
|
87
|
-
});
|
|
128
|
+
examples: ['dino sync --sync-timeout 20000 --format json', 'dino sync --strict --sync-timeout 20000 --format json'],
|
|
88
129
|
},
|
|
130
|
+
action: executeSyncCommand,
|
|
89
131
|
};
|
|
90
132
|
export function registerSyncCommand(program) {
|
|
91
133
|
registerStandaloneCommand(program, syncCommandDescriptor);
|
|
@@ -6,6 +6,7 @@ import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
|
6
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
8
8
|
import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
9
|
+
import { assertWriteConfirmationProvided, parseExpectedCount } from '../writeConfirmation.js';
|
|
9
10
|
import { addTag, inspectAddTag, listTags } from './repo.js';
|
|
10
11
|
import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
|
|
11
12
|
const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -67,7 +68,7 @@ async function runTagAdd(context) {
|
|
|
67
68
|
userId: identity.userId,
|
|
68
69
|
emoji,
|
|
69
70
|
})),
|
|
70
|
-
}));
|
|
71
|
+
}), runtime);
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
const result = await addTag(runtime.db, {
|
|
@@ -114,7 +115,7 @@ async function runTagRename(context) {
|
|
|
114
115
|
summary: `Rename tag "${result.fromPath}" to "${result.toPath}".`,
|
|
115
116
|
inputs: { path, new_name: newName },
|
|
116
117
|
changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
|
|
117
|
-
}));
|
|
118
|
+
}), runtime);
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
await waitForMutationUpload(runtime);
|
|
@@ -139,7 +140,7 @@ async function runTagMove(context) {
|
|
|
139
140
|
summary: `Move tag "${result.fromPath}" to "${result.toPath}".`,
|
|
140
141
|
inputs: { path, to },
|
|
141
142
|
changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
|
|
142
|
-
}));
|
|
143
|
+
}), runtime);
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
await waitForMutationUpload(runtime);
|
|
@@ -149,9 +150,17 @@ async function runTagMove(context) {
|
|
|
149
150
|
async function runTagMerge(context) {
|
|
150
151
|
const from = requireArg(context.args.from, 'Source tag path is required');
|
|
151
152
|
const to = requireArg(context.args.to, 'Target tag path is required');
|
|
153
|
+
const dryRun = Boolean(context.options.dryRun);
|
|
154
|
+
const expectedCount = parseExpectedCount(context.options.expectedCount);
|
|
155
|
+
const writeOptions = {
|
|
156
|
+
dryRun,
|
|
157
|
+
confirm: Boolean(context.options.confirm),
|
|
158
|
+
expectedCount,
|
|
159
|
+
};
|
|
160
|
+
assertWriteConfirmationProvided('Tag merge', writeOptions);
|
|
152
161
|
await withTagWriteRuntime(context, async ({ runtime, userId }) => {
|
|
153
|
-
const result = await mergeTag(runtime.db, { from, to, userId },
|
|
154
|
-
if (
|
|
162
|
+
const result = await mergeTag(runtime.db, { from, to, userId }, writeOptions);
|
|
163
|
+
if (dryRun) {
|
|
155
164
|
printDryRunPreview(buildTagMutationDryRunPreview({
|
|
156
165
|
config: runtime.config,
|
|
157
166
|
offline: runtime.offline,
|
|
@@ -159,12 +168,15 @@ async function runTagMerge(context) {
|
|
|
159
168
|
action: 'merge_tag',
|
|
160
169
|
risk: 'high-risk-write',
|
|
161
170
|
summary: `Merge tag subtree "${result.fromPath}" into "${result.toPath}".`,
|
|
162
|
-
inputs: { from, to },
|
|
171
|
+
inputs: { from, to, expected_count: expectedCount },
|
|
163
172
|
changes: {
|
|
173
|
+
affected_count: result.affectedCount,
|
|
174
|
+
soft_deleted_tag_count: result.softDeletedTagIds.length,
|
|
175
|
+
updated_note_count: result.updatedNotes.length,
|
|
164
176
|
soft_deleted_tag_ids: result.softDeletedTagIds,
|
|
165
177
|
updated_notes: result.updatedNotes,
|
|
166
178
|
},
|
|
167
|
-
}));
|
|
179
|
+
}), runtime);
|
|
168
180
|
return;
|
|
169
181
|
}
|
|
170
182
|
await waitForMutationUpload(runtime);
|
|
@@ -193,7 +205,7 @@ async function runTagCleanup(context) {
|
|
|
193
205
|
summary: `Inspect tag cleanup candidates (${result.issues.length} issues).`,
|
|
194
206
|
inputs: {},
|
|
195
207
|
targets: result.issues,
|
|
196
|
-
}));
|
|
208
|
+
}), runtime);
|
|
197
209
|
return;
|
|
198
210
|
}
|
|
199
211
|
printSuccess({ stale: commandStale(runtime), ...result });
|
|
@@ -328,7 +340,11 @@ export const tagCommandGroup = {
|
|
|
328
340
|
{ syntax: '<from>', description: 'Source tag path' },
|
|
329
341
|
{ syntax: '<to>', description: 'Target tag path' },
|
|
330
342
|
],
|
|
331
|
-
options: [
|
|
343
|
+
options: [
|
|
344
|
+
{ flags: '--expected-count <n>', description: 'Required for real writes; must equal source tags plus notes affected', type: 'number' },
|
|
345
|
+
{ flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
|
|
346
|
+
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
347
|
+
],
|
|
332
348
|
schema: {
|
|
333
349
|
path: 'tag.merge',
|
|
334
350
|
risk: 'high-risk-write',
|
|
@@ -337,9 +353,12 @@ export const tagCommandGroup = {
|
|
|
337
353
|
output: {
|
|
338
354
|
type: 'structured',
|
|
339
355
|
summary: 'Tag merge result',
|
|
340
|
-
fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'stale'],
|
|
356
|
+
fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'affectedCount', 'stale'],
|
|
341
357
|
},
|
|
342
|
-
examples: [
|
|
358
|
+
examples: [
|
|
359
|
+
'dino tag merge "ai" "AI" --format json --dry-run',
|
|
360
|
+
'dino tag merge "ai" "AI" --expected-count 4 --confirm --format json',
|
|
361
|
+
],
|
|
343
362
|
},
|
|
344
363
|
action: runTagMerge,
|
|
345
364
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type ConfirmedWriteOptions } from '../writeConfirmation.js';
|
|
2
3
|
import type { TagListItem } from './model.js';
|
|
3
4
|
export type TagTreeNode = TagListItem & {
|
|
4
5
|
isActual: boolean;
|
|
@@ -63,6 +64,7 @@ export type TagMergeResult = {
|
|
|
63
64
|
before: string[];
|
|
64
65
|
after: string[];
|
|
65
66
|
}>;
|
|
67
|
+
affectedCount: number;
|
|
66
68
|
};
|
|
67
69
|
export type TagSuggestResult = {
|
|
68
70
|
suggestions: Array<{
|
|
@@ -98,8 +100,6 @@ export declare function renameTag(db: AbstractPowerSyncDatabase, input: TagRenam
|
|
|
98
100
|
export declare function moveTag(db: AbstractPowerSyncDatabase, input: TagMoveInput, options?: {
|
|
99
101
|
dryRun?: boolean;
|
|
100
102
|
}): Promise<TagRenameMoveResult>;
|
|
101
|
-
export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?:
|
|
102
|
-
dryRun?: boolean;
|
|
103
|
-
}): Promise<TagMergeResult>;
|
|
103
|
+
export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?: ConfirmedWriteOptions): Promise<TagMergeResult>;
|
|
104
104
|
export declare function inspectTagCleanup(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagCleanupResult>;
|
|
105
105
|
export declare function suggestTagMerges(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagSuggestResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
2
2
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
3
|
import { nowIsoUtc } from '../../utils/time.js';
|
|
4
|
+
import { assertConfirmedWrite, assertWriteConfirmationProvided, } from '../writeConfirmation.js';
|
|
4
5
|
import { mergeTagReferencesInNote, renameTagReferencesInNote } from './content.js';
|
|
5
6
|
import { MAX_TAG_SEGMENT_LENGTH, REQUIRED_NOTE_TAG_COLUMNS, REQUIRED_TAG_NODE_COLUMNS, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, isTagPathWithinPrefix, normalizeTagLookupKey, parseStoredTags, parentPathOf, renameTagPathWithPrefix, trimString, } from './model.js';
|
|
6
7
|
import { activeTagNodes, actualActiveTagNodes, assertCanWriteTag, assertRequiredColumns, assertWritableTagNodes, descendantsOf, ensureNoTagPathConflict, getTableColumns, loadTagNodes, parentIdForPath, requireActiveTagPath, toTagListItem, } from './store.js';
|
|
@@ -297,7 +298,7 @@ export async function moveTag(db, input, options = {}) {
|
|
|
297
298
|
updatedNotes: noteUpdates.map(({ id, before, after }) => ({ id, before, after })),
|
|
298
299
|
};
|
|
299
300
|
}
|
|
300
|
-
|
|
301
|
+
async function buildTagMergePlan(db, input) {
|
|
301
302
|
const from = assertPath(input.from, 'Source tag path');
|
|
302
303
|
const to = assertPath(input.to, 'Target tag path');
|
|
303
304
|
const userId = trimString(input.userId);
|
|
@@ -318,28 +319,52 @@ export async function mergeTag(db, input, options = {}) {
|
|
|
318
319
|
.map((row) => updateNoteTagsForPrefix(row, source.fullPath, target.fullPath, 'merge', lookup))
|
|
319
320
|
.filter((row) => row.id && row.changed);
|
|
320
321
|
const softDeletedTagIds = sourceNodes.map((node) => node.id);
|
|
321
|
-
|
|
322
|
-
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
323
|
-
const timestamp = nowIsoUtc();
|
|
324
|
-
await applyNoteUpdates(tx, noteUpdates, timestamp);
|
|
325
|
-
for (const tagId of softDeletedTagIds) {
|
|
326
|
-
await tx.execute(`
|
|
327
|
-
UPDATE c_tag_node
|
|
328
|
-
SET is_del = 1, updated_at = ?
|
|
329
|
-
WHERE id = ?
|
|
330
|
-
`, [timestamp, tagId]);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
}
|
|
322
|
+
const affectedCount = softDeletedTagIds.length + noteUpdates.length;
|
|
334
323
|
return {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
324
|
+
result: {
|
|
325
|
+
fromId: source.id,
|
|
326
|
+
toId: target.id,
|
|
327
|
+
fromPath: source.fullPath,
|
|
328
|
+
toPath: target.fullPath,
|
|
329
|
+
softDeletedTagIds,
|
|
330
|
+
updatedNotes: noteUpdates.map(({ id, before, after }) => ({ id, before, after })),
|
|
331
|
+
affectedCount,
|
|
332
|
+
},
|
|
333
|
+
noteUpdates,
|
|
334
|
+
userId,
|
|
341
335
|
};
|
|
342
336
|
}
|
|
337
|
+
function assertTagMergeCount(plan, options) {
|
|
338
|
+
assertConfirmedWrite({
|
|
339
|
+
operation: 'Tag merge',
|
|
340
|
+
schemaPath: 'tag.merge',
|
|
341
|
+
actualCount: plan.result.affectedCount,
|
|
342
|
+
options,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
export async function mergeTag(db, input, options = {}) {
|
|
346
|
+
if (options.dryRun) {
|
|
347
|
+
const plan = await buildTagMergePlan(db, input);
|
|
348
|
+
assertTagMergeCount(plan, options);
|
|
349
|
+
return plan.result;
|
|
350
|
+
}
|
|
351
|
+
assertWriteConfirmationProvided('Tag merge', options);
|
|
352
|
+
return withPowerSyncWriteTransaction(db, async (tx) => {
|
|
353
|
+
const plan = await buildTagMergePlan(tx, input);
|
|
354
|
+
assertTagMergeCount(plan, options);
|
|
355
|
+
const timestamp = nowIsoUtc();
|
|
356
|
+
await applyNoteUpdates(tx, plan.noteUpdates, timestamp);
|
|
357
|
+
for (const tagId of plan.result.softDeletedTagIds) {
|
|
358
|
+
await tx.execute(`
|
|
359
|
+
UPDATE c_tag_node
|
|
360
|
+
SET is_del = 1, updated_at = ?
|
|
361
|
+
WHERE id = ?
|
|
362
|
+
AND TRIM(COALESCE(user_id, '')) = ?
|
|
363
|
+
`, [timestamp, tagId, plan.userId]);
|
|
364
|
+
}
|
|
365
|
+
return plan.result;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
343
368
|
export async function inspectTagCleanup(db, input = {}) {
|
|
344
369
|
const columns = await getTableColumns(db, 'c_tag_node');
|
|
345
370
|
assertRequiredColumns('c_tag_node', columns, REQUIRED_TAG_NODE_COLUMNS);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type TiptapNode } from '../notes/contentEngine.js';
|
|
2
|
+
export declare const NOTE_TEMPLATE_PROPERTY_DEF_VERSION = 1;
|
|
3
|
+
export declare const MAX_NOTE_TEMPLATE_PROPERTY_KEYS = 50;
|
|
4
|
+
export declare const MAX_NOTE_TEMPLATE_CONTENT_LENGTH = 200000;
|
|
5
|
+
export type NoteTemplatePropertyDef = {
|
|
6
|
+
version: typeof NOTE_TEMPLATE_PROPERTY_DEF_VERSION;
|
|
7
|
+
keys: string[];
|
|
8
|
+
};
|
|
9
|
+
export type NoteTemplateRow = {
|
|
10
|
+
id: string;
|
|
11
|
+
user_id: string | null;
|
|
12
|
+
name: string | null;
|
|
13
|
+
content: string | null;
|
|
14
|
+
priority: number | null;
|
|
15
|
+
is_show: number | null;
|
|
16
|
+
extra_data: string | null;
|
|
17
|
+
property_def: string | null;
|
|
18
|
+
insert_at: string | null;
|
|
19
|
+
update_at: string | null;
|
|
20
|
+
is_del: number | null;
|
|
21
|
+
};
|
|
22
|
+
export type NoteTemplateSummary = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
propertyKeys: string[];
|
|
26
|
+
content: string;
|
|
27
|
+
hasContent: boolean;
|
|
28
|
+
isShow: boolean;
|
|
29
|
+
priority: number;
|
|
30
|
+
updateAt: string | null;
|
|
31
|
+
};
|
|
32
|
+
export type NoteTemplateTokenValues = {
|
|
33
|
+
date: string;
|
|
34
|
+
datetime: string;
|
|
35
|
+
time: string;
|
|
36
|
+
location: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function isNotePropertyKeyValid(value: unknown): value is string;
|
|
39
|
+
export declare function validateNoteTemplatePropertyKeys(keys: readonly unknown[]): string[];
|
|
40
|
+
export declare function parseNoteTemplatePropertyDef(raw: string | null | undefined): NoteTemplatePropertyDef | null;
|
|
41
|
+
export declare function serializeNoteTemplatePropertyDef(keys: readonly string[]): string | null;
|
|
42
|
+
export declare function isNoteTemplateDocumentEmpty(document: TiptapNode | null | undefined): boolean;
|
|
43
|
+
export declare function parseNoteTemplateContent(raw: string | null | undefined): TiptapNode | null;
|
|
44
|
+
export declare function serializeNoteTemplateContent(document: unknown): string;
|
|
45
|
+
export declare function buildNoteTemplateTokenValues(now?: Date, location?: string): NoteTemplateTokenValues;
|
|
46
|
+
export declare function hydrateNoteTemplateDocument(document: TiptapNode, options?: {
|
|
47
|
+
now?: Date;
|
|
48
|
+
location?: string;
|
|
49
|
+
}): TiptapNode;
|
|
50
|
+
export declare function mapNoteTemplateRow(row: NoteTemplateRow): NoteTemplateSummary;
|