@bbigbang/core 0.1.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/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export function insertPanel(db, params) {
|
|
3
|
+
const id = params.id ?? randomUUID();
|
|
4
|
+
const now = params.createdAt ?? Date.now();
|
|
5
|
+
db.prepare(`INSERT INTO panels(
|
|
6
|
+
id, agent_id, conversation_id, owner_user_id, handle, scope_type, scope_id,
|
|
7
|
+
component, props_json, actions_json, dataset_source_json, row_count, status, progress_json, result_json, version,
|
|
8
|
+
created_by_run_id, created_at, updated_at
|
|
9
|
+
)
|
|
10
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.agentId, params.conversationId, params.ownerUserId ?? null, normalizePanelHandle(params.handle), params.scopeType, params.scopeId ?? null, params.component, JSON.stringify(params.props ?? {}), JSON.stringify(params.actions ?? []), params.datasetSource ? JSON.stringify(params.datasetSource) : null, params.rowCount ?? 0, params.status ?? 'idle', params.progress == null ? null : JSON.stringify(params.progress), params.result == null ? null : JSON.stringify(params.result), params.version ?? 1, params.createdByRunId ?? null, now, params.updatedAt ?? now);
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
function normalizePanelHandle(handle) {
|
|
14
|
+
const normalized = handle?.trim() ?? '';
|
|
15
|
+
return normalized || null;
|
|
16
|
+
}
|
|
17
|
+
export function insertPanelRows(db, rows) {
|
|
18
|
+
const insert = db.prepare(`INSERT INTO panel_rows(
|
|
19
|
+
panel_id, row_index, row_id, fields_json, media_json,
|
|
20
|
+
node_id, cached_asset_ids_json
|
|
21
|
+
)
|
|
22
|
+
VALUES(?, ?, ?, ?, ?, ?, ?)`);
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
insert.run(row.panelId, row.rowIndex, row.rowId ?? null, JSON.stringify(row.fields), JSON.stringify(row.media), row.nodeId ?? null, JSON.stringify(row.cachedAssetIds ?? {}));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getPanelById(db, id) {
|
|
28
|
+
const row = db.prepare(`SELECT
|
|
29
|
+
id, agent_id as agentId, conversation_id as conversationId, owner_user_id as ownerUserId,
|
|
30
|
+
handle,
|
|
31
|
+
scope_type as scopeType, scope_id as scopeId,
|
|
32
|
+
component, props_json as propsJson, actions_json as actionsJson,
|
|
33
|
+
dataset_source_json as datasetSourceJson,
|
|
34
|
+
row_count as rowCount, status, progress_json as progressJson,
|
|
35
|
+
result_json as resultJson, version, created_by_run_id as createdByRunId,
|
|
36
|
+
created_at as createdAt, updated_at as updatedAt, archived_at as archivedAt
|
|
37
|
+
FROM panels
|
|
38
|
+
WHERE id = ?
|
|
39
|
+
LIMIT 1`).get(id);
|
|
40
|
+
if (!row)
|
|
41
|
+
return null;
|
|
42
|
+
const datasetSource = parseNullableJsonObject(row.datasetSourceJson);
|
|
43
|
+
return {
|
|
44
|
+
id: row.id,
|
|
45
|
+
agentId: row.agentId,
|
|
46
|
+
conversationId: row.conversationId,
|
|
47
|
+
ownerUserId: row.ownerUserId ?? undefined,
|
|
48
|
+
handle: row.handle,
|
|
49
|
+
scopeType: row.scopeType,
|
|
50
|
+
scopeId: row.scopeId ?? undefined,
|
|
51
|
+
toolId: row.scopeType === 'tool' ? (row.scopeId ?? undefined) : undefined,
|
|
52
|
+
component: row.component,
|
|
53
|
+
props: parseJsonObject(row.propsJson),
|
|
54
|
+
actions: parseJsonArray(row.actionsJson),
|
|
55
|
+
rowCount: row.rowCount,
|
|
56
|
+
rowCountKnown: row.rowCount >= 0,
|
|
57
|
+
datasetSource: datasetSource ?? undefined,
|
|
58
|
+
status: normalizePanelStatus(row.status),
|
|
59
|
+
progress: parseNullableJsonObject(row.progressJson),
|
|
60
|
+
result: parseNullableJsonObject(row.resultJson),
|
|
61
|
+
version: row.version,
|
|
62
|
+
createdByRunId: row.createdByRunId ?? undefined,
|
|
63
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
64
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
65
|
+
archivedAt: row.archivedAt == null ? null : new Date(row.archivedAt).toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function getPanelByStableHandle(db, params) {
|
|
69
|
+
const ownerUserId = params.ownerUserId?.trim() ?? '';
|
|
70
|
+
const handle = normalizePanelHandle(params.handle);
|
|
71
|
+
if (!ownerUserId || !handle)
|
|
72
|
+
return null;
|
|
73
|
+
const row = db.prepare(`SELECT id
|
|
74
|
+
FROM panels
|
|
75
|
+
WHERE owner_user_id = ?
|
|
76
|
+
AND agent_id = ?
|
|
77
|
+
AND conversation_id = ?
|
|
78
|
+
AND handle = ?
|
|
79
|
+
AND archived_at IS NULL
|
|
80
|
+
LIMIT 1`).get(ownerUserId, params.agentId, params.conversationId, handle);
|
|
81
|
+
return row ? getPanelById(db, row.id) : null;
|
|
82
|
+
}
|
|
83
|
+
export function deletePanel(db, id) {
|
|
84
|
+
const result = db.prepare('DELETE FROM panels WHERE id = ?').run(id);
|
|
85
|
+
return result.changes > 0;
|
|
86
|
+
}
|
|
87
|
+
export function getPanelState(db, panelId, userId) {
|
|
88
|
+
const row = db.prepare(`SELECT panel_id as panelId, user_id as userId,
|
|
89
|
+
state_json as stateJson, updated_at as updatedAt
|
|
90
|
+
FROM panel_state
|
|
91
|
+
WHERE panel_id = ? AND user_id = ?
|
|
92
|
+
LIMIT 1`).get(panelId, userId);
|
|
93
|
+
if (!row)
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
panelId: row.panelId,
|
|
97
|
+
userId: row.userId,
|
|
98
|
+
state: parseJsonObject(row.stateJson),
|
|
99
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function upsertPanelState(db, panelId, userId, state) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
db.prepare(`INSERT INTO panel_state(panel_id, user_id, state_json, updated_at)
|
|
105
|
+
VALUES(?, ?, ?, ?)
|
|
106
|
+
ON CONFLICT(panel_id, user_id)
|
|
107
|
+
DO UPDATE SET state_json = excluded.state_json, updated_at = excluded.updated_at`).run(panelId, userId, JSON.stringify(state), now);
|
|
108
|
+
}
|
|
109
|
+
export function listPanelsForConversation(db, conversationId) {
|
|
110
|
+
const rows = db.prepare(`SELECT
|
|
111
|
+
id, agent_id as agentId, conversation_id as conversationId, owner_user_id as ownerUserId,
|
|
112
|
+
handle,
|
|
113
|
+
scope_type as scopeType, scope_id as scopeId,
|
|
114
|
+
component, props_json as propsJson, actions_json as actionsJson,
|
|
115
|
+
dataset_source_json as datasetSourceJson,
|
|
116
|
+
row_count as rowCount, status, progress_json as progressJson,
|
|
117
|
+
result_json as resultJson, version, created_by_run_id as createdByRunId,
|
|
118
|
+
created_at as createdAt, updated_at as updatedAt, archived_at as archivedAt
|
|
119
|
+
FROM panels
|
|
120
|
+
WHERE conversation_id = ?
|
|
121
|
+
ORDER BY created_at DESC`).all(conversationId);
|
|
122
|
+
return rows.map((row) => ({
|
|
123
|
+
id: row.id,
|
|
124
|
+
agentId: row.agentId,
|
|
125
|
+
conversationId: row.conversationId,
|
|
126
|
+
ownerUserId: row.ownerUserId ?? undefined,
|
|
127
|
+
handle: row.handle,
|
|
128
|
+
scopeType: row.scopeType,
|
|
129
|
+
scopeId: row.scopeId ?? undefined,
|
|
130
|
+
toolId: row.scopeType === 'tool' ? (row.scopeId ?? undefined) : undefined,
|
|
131
|
+
component: row.component,
|
|
132
|
+
props: parseJsonObject(row.propsJson),
|
|
133
|
+
actions: parseJsonArray(row.actionsJson),
|
|
134
|
+
rowCount: row.rowCount,
|
|
135
|
+
rowCountKnown: row.rowCount >= 0,
|
|
136
|
+
datasetSource: parseNullableJsonObject(row.datasetSourceJson) ?? undefined,
|
|
137
|
+
status: normalizePanelStatus(row.status),
|
|
138
|
+
progress: parseNullableJsonObject(row.progressJson),
|
|
139
|
+
result: parseNullableJsonObject(row.resultJson),
|
|
140
|
+
version: row.version,
|
|
141
|
+
createdByRunId: row.createdByRunId ?? undefined,
|
|
142
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
143
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
144
|
+
archivedAt: row.archivedAt == null ? null : new Date(row.archivedAt).toISOString(),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
export function countPanelRows(db, panelId) {
|
|
148
|
+
const row = db.prepare(`SELECT COUNT(*) as count FROM panel_rows WHERE panel_id = ?`).get(panelId);
|
|
149
|
+
return row?.count ?? 0;
|
|
150
|
+
}
|
|
151
|
+
function parseJsonObject(value) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(value);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parseNullableJsonObject(value) {
|
|
160
|
+
if (!value)
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(value);
|
|
164
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
165
|
+
? parsed
|
|
166
|
+
: null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function normalizePanelStatus(value) {
|
|
173
|
+
if (value === 'running' || value === 'completed' || value === 'failed' || value === 'idle') {
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
if (value === 'ready')
|
|
177
|
+
return 'idle';
|
|
178
|
+
return 'idle';
|
|
179
|
+
}
|
|
180
|
+
const PANEL_FIELD_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_:-]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_:-]{0,63}){0,15}$/;
|
|
181
|
+
export function getPanelFieldValue(fields, fieldPath) {
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(fields, fieldPath)) {
|
|
183
|
+
return fields[fieldPath];
|
|
184
|
+
}
|
|
185
|
+
if (!fieldPath.includes('.')) {
|
|
186
|
+
return fields[fieldPath];
|
|
187
|
+
}
|
|
188
|
+
let current = fields;
|
|
189
|
+
for (const segment of fieldPath.split('.')) {
|
|
190
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const record = current;
|
|
194
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
current = record[segment];
|
|
198
|
+
}
|
|
199
|
+
return current;
|
|
200
|
+
}
|
|
201
|
+
function quotedJsonPathForField(fieldPath, mode) {
|
|
202
|
+
if (!PANEL_FIELD_PATH_PATTERN.test(fieldPath)) {
|
|
203
|
+
throw new Error(`Unsafe panel field path: ${fieldPath}`);
|
|
204
|
+
}
|
|
205
|
+
const segments = mode === 'direct' ? [fieldPath] : fieldPath.split('.');
|
|
206
|
+
return `$${segments.map((segment) => `."${segment}"`).join('')}`;
|
|
207
|
+
}
|
|
208
|
+
function panelFieldJsonExtractExpression(fieldPath) {
|
|
209
|
+
const directPath = quotedJsonPathForField(fieldPath, 'direct');
|
|
210
|
+
const nestedPath = quotedJsonPathForField(fieldPath, 'nested');
|
|
211
|
+
const directExtract = `json_extract(fields_json, '${directPath}')`;
|
|
212
|
+
if (directPath === nestedPath)
|
|
213
|
+
return directExtract;
|
|
214
|
+
return `COALESCE(${directExtract}, json_extract(fields_json, '${nestedPath}'))`;
|
|
215
|
+
}
|
|
216
|
+
function toSqlJsonValue(value) {
|
|
217
|
+
if (typeof value === 'boolean')
|
|
218
|
+
return value ? 1 : 0;
|
|
219
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
220
|
+
return value;
|
|
221
|
+
if (typeof value === 'string')
|
|
222
|
+
return value;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
export function listPanelRows(db, panelId, options) {
|
|
226
|
+
const limit = Math.min(Math.max(1, options.limit), 200);
|
|
227
|
+
let cursorData = null;
|
|
228
|
+
if (options.cursor) {
|
|
229
|
+
try {
|
|
230
|
+
cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString('utf8'));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
cursorData = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const conditions = ['panel_id = ?'];
|
|
237
|
+
const params = [panelId];
|
|
238
|
+
if (options.filterField && options.filterValue !== undefined) {
|
|
239
|
+
conditions.push(`${panelFieldJsonExtractExpression(options.filterField)} = ?`);
|
|
240
|
+
params.push(toSqlJsonValue(options.filterValue));
|
|
241
|
+
}
|
|
242
|
+
if (options.sortField && options.sortDirection) {
|
|
243
|
+
const sortField = options.sortField;
|
|
244
|
+
const direction = options.sortDirection;
|
|
245
|
+
const isNumericSort = options.sortFieldType === 'number';
|
|
246
|
+
const fieldExtract = panelFieldJsonExtractExpression(sortField);
|
|
247
|
+
const extract = isNumericSort
|
|
248
|
+
? `COALESCE(CAST(${fieldExtract} AS REAL), 0)`
|
|
249
|
+
: fieldExtract;
|
|
250
|
+
if (cursorData && cursorData.ri !== undefined) {
|
|
251
|
+
if (direction === 'asc') {
|
|
252
|
+
conditions.push(`(${extract} > ? OR (${extract} = ? AND row_index > ?))`);
|
|
253
|
+
const sortValue = toSqlJsonValue(cursorData.sv);
|
|
254
|
+
params.push(sortValue, sortValue, cursorData.ri);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
conditions.push(`(${extract} < ? OR (${extract} = ? AND row_index < ?))`);
|
|
258
|
+
const sortValue = toSqlJsonValue(cursorData.sv);
|
|
259
|
+
params.push(sortValue, sortValue, cursorData.ri);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const orderBy = direction === 'asc' ? 'ASC' : 'DESC';
|
|
263
|
+
const tieBreaker = direction === 'asc' ? 'ASC' : 'DESC';
|
|
264
|
+
const sql = `SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson
|
|
265
|
+
FROM panel_rows
|
|
266
|
+
WHERE ${conditions.join(' AND ')}
|
|
267
|
+
ORDER BY ${extract} ${orderBy}, row_index ${tieBreaker}
|
|
268
|
+
LIMIT ?`;
|
|
269
|
+
params.push(limit + 1);
|
|
270
|
+
const rows = db.prepare(sql).all(...params);
|
|
271
|
+
const hasMore = rows.length > limit;
|
|
272
|
+
const resultRows = hasMore ? rows.slice(0, limit) : rows;
|
|
273
|
+
const lastRow = resultRows[resultRows.length - 1];
|
|
274
|
+
const nextCursor = hasMore && lastRow
|
|
275
|
+
? Buffer.from(JSON.stringify({
|
|
276
|
+
ri: lastRow.rowIndex,
|
|
277
|
+
sv: getPanelFieldValue(parseJsonObject(lastRow.fieldsJson), sortField),
|
|
278
|
+
}), 'utf8').toString('base64')
|
|
279
|
+
: null;
|
|
280
|
+
return {
|
|
281
|
+
rows: resultRows.map((r) => ({
|
|
282
|
+
rowIndex: r.rowIndex,
|
|
283
|
+
rowId: r.rowId,
|
|
284
|
+
fields: parseJsonObject(r.fieldsJson),
|
|
285
|
+
media: parseJsonObject(r.mediaJson),
|
|
286
|
+
})),
|
|
287
|
+
nextCursor,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Default sort by row_index ASC
|
|
291
|
+
if (cursorData && cursorData.ri !== undefined) {
|
|
292
|
+
conditions.push('row_index > ?');
|
|
293
|
+
params.push(cursorData.ri);
|
|
294
|
+
}
|
|
295
|
+
const sql = `SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson
|
|
296
|
+
FROM panel_rows
|
|
297
|
+
WHERE ${conditions.join(' AND ')}
|
|
298
|
+
ORDER BY row_index ASC
|
|
299
|
+
LIMIT ?`;
|
|
300
|
+
params.push(limit + 1);
|
|
301
|
+
const rows = db.prepare(sql).all(...params);
|
|
302
|
+
const hasMore = rows.length > limit;
|
|
303
|
+
const resultRows = hasMore ? rows.slice(0, limit) : rows;
|
|
304
|
+
const lastRow = resultRows[resultRows.length - 1];
|
|
305
|
+
const nextCursor = hasMore && lastRow
|
|
306
|
+
? Buffer.from(JSON.stringify({ ri: lastRow.rowIndex }), 'utf8').toString('base64')
|
|
307
|
+
: null;
|
|
308
|
+
return {
|
|
309
|
+
rows: resultRows.map((r) => ({
|
|
310
|
+
rowIndex: r.rowIndex,
|
|
311
|
+
rowId: r.rowId,
|
|
312
|
+
fields: parseJsonObject(r.fieldsJson),
|
|
313
|
+
media: parseJsonObject(r.mediaJson),
|
|
314
|
+
})),
|
|
315
|
+
nextCursor,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
export function aggregatePanelRows(db, panelId, options) {
|
|
319
|
+
const conditions = ['panel_id = ?'];
|
|
320
|
+
const params = [panelId];
|
|
321
|
+
if (options.filterField && options.filterValue !== undefined) {
|
|
322
|
+
conditions.push(`${panelFieldJsonExtractExpression(options.filterField)} = ?`);
|
|
323
|
+
params.push(toSqlJsonValue(options.filterValue));
|
|
324
|
+
}
|
|
325
|
+
const rows = db.prepare(`SELECT fields_json as fieldsJson
|
|
326
|
+
FROM panel_rows
|
|
327
|
+
WHERE ${conditions.join(' AND ')}
|
|
328
|
+
ORDER BY row_index ASC`).all(...params);
|
|
329
|
+
return aggregatePanelFieldRecords(rows.map((row) => parseJsonObject(row.fieldsJson)), {
|
|
330
|
+
op: options.op,
|
|
331
|
+
field: options.field,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
export function aggregatePanelFieldRecords(rows, options) {
|
|
335
|
+
const totalCount = rows.length;
|
|
336
|
+
if (options.op === 'count' && !options.field) {
|
|
337
|
+
return {
|
|
338
|
+
op: options.op,
|
|
339
|
+
field: null,
|
|
340
|
+
value: totalCount,
|
|
341
|
+
count: totalCount,
|
|
342
|
+
totalCount,
|
|
343
|
+
hasValue: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const field = options.field ?? null;
|
|
347
|
+
if (!field) {
|
|
348
|
+
return {
|
|
349
|
+
op: options.op,
|
|
350
|
+
field,
|
|
351
|
+
value: null,
|
|
352
|
+
count: 0,
|
|
353
|
+
totalCount,
|
|
354
|
+
hasValue: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (options.op === 'count') {
|
|
358
|
+
const count = rows.filter((fields) => hasNonEmptyPanelValue(getPanelFieldValue(fields, field))).length;
|
|
359
|
+
return {
|
|
360
|
+
op: options.op,
|
|
361
|
+
field,
|
|
362
|
+
value: count,
|
|
363
|
+
count,
|
|
364
|
+
totalCount,
|
|
365
|
+
hasValue: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const values = rows
|
|
369
|
+
.map((fields) => toFinitePanelNumber(getPanelFieldValue(fields, field)))
|
|
370
|
+
.filter((value) => value !== null);
|
|
371
|
+
if (values.length === 0) {
|
|
372
|
+
return {
|
|
373
|
+
op: options.op,
|
|
374
|
+
field,
|
|
375
|
+
value: null,
|
|
376
|
+
count: 0,
|
|
377
|
+
totalCount,
|
|
378
|
+
hasValue: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
let value;
|
|
382
|
+
switch (options.op) {
|
|
383
|
+
case 'sum':
|
|
384
|
+
value = values.reduce((total, item) => total + item, 0);
|
|
385
|
+
break;
|
|
386
|
+
case 'avg':
|
|
387
|
+
value = values.reduce((total, item) => total + item, 0) / values.length;
|
|
388
|
+
break;
|
|
389
|
+
case 'min':
|
|
390
|
+
value = Math.min(...values);
|
|
391
|
+
break;
|
|
392
|
+
case 'max':
|
|
393
|
+
value = Math.max(...values);
|
|
394
|
+
break;
|
|
395
|
+
default:
|
|
396
|
+
value = Number.NaN;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
op: options.op,
|
|
401
|
+
field,
|
|
402
|
+
value: Number.isFinite(value) ? value : null,
|
|
403
|
+
count: values.length,
|
|
404
|
+
totalCount,
|
|
405
|
+
hasValue: Number.isFinite(value),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function hasNonEmptyPanelValue(value) {
|
|
409
|
+
if (value === null || value === undefined)
|
|
410
|
+
return false;
|
|
411
|
+
if (typeof value === 'string')
|
|
412
|
+
return value.trim().length > 0;
|
|
413
|
+
if (Array.isArray(value))
|
|
414
|
+
return value.length > 0;
|
|
415
|
+
if (typeof value === 'object')
|
|
416
|
+
return Object.keys(value).length > 0;
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
function toFinitePanelNumber(value) {
|
|
420
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
421
|
+
return value;
|
|
422
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
423
|
+
const parsed = Number(value);
|
|
424
|
+
if (Number.isFinite(parsed))
|
|
425
|
+
return parsed;
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
export function validateStateBlob(stateSchema, blob) {
|
|
430
|
+
const errors = [];
|
|
431
|
+
if (typeof blob !== 'object' || blob === null || Array.isArray(blob)) {
|
|
432
|
+
errors.push('State must be an object');
|
|
433
|
+
return errors;
|
|
434
|
+
}
|
|
435
|
+
const schemaObj = stateSchema;
|
|
436
|
+
if (schemaObj?.type === 'object' && schemaObj.properties) {
|
|
437
|
+
for (const [key, propSchema] of Object.entries(schemaObj.properties)) {
|
|
438
|
+
const value = blob[key];
|
|
439
|
+
if (value === undefined)
|
|
440
|
+
continue;
|
|
441
|
+
const expectedType = propSchema.type;
|
|
442
|
+
if (!expectedType)
|
|
443
|
+
continue;
|
|
444
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
445
|
+
if (actualType !== expectedType) {
|
|
446
|
+
errors.push(`Property "${key}" must be of type ${expectedType}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return errors;
|
|
451
|
+
}
|
|
452
|
+
function parseJsonArray(value) {
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(value);
|
|
455
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
export function getPanelRowByIndex(db, panelId, rowIndex) {
|
|
462
|
+
const row = db.prepare(`SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson,
|
|
463
|
+
node_id as nodeId, cached_asset_ids_json as cachedAssetIdsJson
|
|
464
|
+
FROM panel_rows
|
|
465
|
+
WHERE panel_id = ? AND row_index = ?
|
|
466
|
+
LIMIT 1`).get(panelId, rowIndex);
|
|
467
|
+
if (!row)
|
|
468
|
+
return null;
|
|
469
|
+
return {
|
|
470
|
+
rowIndex: row.rowIndex,
|
|
471
|
+
rowId: row.rowId,
|
|
472
|
+
fields: parseJsonObject(row.fieldsJson),
|
|
473
|
+
media: parseJsonObject(row.mediaJson),
|
|
474
|
+
nodeId: row.nodeId,
|
|
475
|
+
cachedAssetIds: parseJsonObject(row.cachedAssetIdsJson),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
export function generatePanelRowId(db, panelId, preferredIndex) {
|
|
479
|
+
const existingRows = db.prepare(`SELECT row_id as rowId FROM panel_rows WHERE panel_id = ? AND row_id IS NOT NULL`).all(panelId);
|
|
480
|
+
const existing = new Set(existingRows.map((row) => row.rowId));
|
|
481
|
+
let candidate = `row-${preferredIndex}`;
|
|
482
|
+
let suffix = 1;
|
|
483
|
+
while (existing.has(candidate)) {
|
|
484
|
+
candidate = `row-${preferredIndex}-${suffix}`;
|
|
485
|
+
suffix += 1;
|
|
486
|
+
}
|
|
487
|
+
return candidate;
|
|
488
|
+
}
|
|
489
|
+
export function patchPanel(db, params) {
|
|
490
|
+
const current = getPanelById(db, params.panelId);
|
|
491
|
+
if (!current) {
|
|
492
|
+
return { ok: false, failureClass: 'panel_not_found', details: { panelId: params.panelId } };
|
|
493
|
+
}
|
|
494
|
+
if (current.archivedAt) {
|
|
495
|
+
return { ok: false, failureClass: 'panel_archived', details: { panelId: params.panelId } };
|
|
496
|
+
}
|
|
497
|
+
if (params.expectedVersion !== undefined && params.expectedVersion !== current.version) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
failureClass: 'version_conflict',
|
|
501
|
+
details: {
|
|
502
|
+
panelId: params.panelId,
|
|
503
|
+
expectedVersion: params.expectedVersion,
|
|
504
|
+
currentVersion: current.version,
|
|
505
|
+
recommendedAction: 'Read the latest panel state/events/rows, merge user changes, then retry with the current version. Do not blindly overwrite saved user state or annotations.',
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const changed = new Set();
|
|
510
|
+
const nextProps = params.props !== undefined
|
|
511
|
+
? params.props
|
|
512
|
+
: params.propsPatch
|
|
513
|
+
? { ...(current.props ?? {}), ...params.propsPatch }
|
|
514
|
+
: current.props ?? {};
|
|
515
|
+
if (params.props !== undefined || params.propsPatch)
|
|
516
|
+
changed.add('props');
|
|
517
|
+
const nextActions = params.actions ?? current.actions ?? [];
|
|
518
|
+
if (params.actions)
|
|
519
|
+
changed.add('actions');
|
|
520
|
+
const nextStatus = params.status ?? current.status;
|
|
521
|
+
if (params.status !== undefined)
|
|
522
|
+
changed.add('status');
|
|
523
|
+
const nextProgress = params.progress === undefined ? current.progress ?? null : params.progress;
|
|
524
|
+
if (params.progress !== undefined)
|
|
525
|
+
changed.add('progress');
|
|
526
|
+
const nextResult = params.result === undefined ? current.result ?? null : params.result;
|
|
527
|
+
if (params.result !== undefined)
|
|
528
|
+
changed.add('result');
|
|
529
|
+
const currentRows = listAllPanelRows(db, params.panelId);
|
|
530
|
+
let nextRows = currentRows;
|
|
531
|
+
if (params.rows) {
|
|
532
|
+
const rowPatch = params.rows.replace
|
|
533
|
+
? {
|
|
534
|
+
ok: true,
|
|
535
|
+
rows: params.rows.replace.map((row, index) => toPanelRowMedia(params.panelId, index, normalizePatchRowId(row.rowId) ?? generatePanelRowId(db, params.panelId, index), row, params.defaultNodeId ?? null)),
|
|
536
|
+
}
|
|
537
|
+
: applyRowPatch(db, params.panelId, nextRows, params.rows, params.defaultNodeId ?? null);
|
|
538
|
+
if (!rowPatch.ok)
|
|
539
|
+
return rowPatch;
|
|
540
|
+
nextRows = rowPatch.rows;
|
|
541
|
+
changed.add('rows');
|
|
542
|
+
}
|
|
543
|
+
if (changed.size === 0) {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
failureClass: 'empty_patch',
|
|
547
|
+
details: { panelId: params.panelId, issue: 'At least one patch field is required.' },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const validation = params.validate?.({
|
|
551
|
+
panel: current,
|
|
552
|
+
props: nextProps,
|
|
553
|
+
actions: nextActions,
|
|
554
|
+
rows: nextRows,
|
|
555
|
+
status: nextStatus,
|
|
556
|
+
progress: nextProgress,
|
|
557
|
+
result: nextResult,
|
|
558
|
+
});
|
|
559
|
+
if (validation && !validation.ok) {
|
|
560
|
+
return validation;
|
|
561
|
+
}
|
|
562
|
+
const now = Date.now();
|
|
563
|
+
const nextVersion = current.version + 1;
|
|
564
|
+
let updateChanges = 0;
|
|
565
|
+
db.transaction(() => {
|
|
566
|
+
const updateResult = db.prepare(`UPDATE panels
|
|
567
|
+
SET props_json = ?,
|
|
568
|
+
actions_json = ?,
|
|
569
|
+
row_count = ?,
|
|
570
|
+
status = ?,
|
|
571
|
+
progress_json = ?,
|
|
572
|
+
result_json = ?,
|
|
573
|
+
dataset_source_json = ?,
|
|
574
|
+
version = ?,
|
|
575
|
+
updated_at = ?
|
|
576
|
+
WHERE id = ? AND version = ? AND archived_at IS NULL`).run(JSON.stringify(nextProps), JSON.stringify(nextActions), Number.isInteger(params.rowCount) ? params.rowCount : nextRows.length, nextStatus, nextProgress == null ? null : JSON.stringify(nextProgress), nextResult == null ? null : JSON.stringify(nextResult), params.datasetSource === undefined
|
|
577
|
+
? current.datasetSource ? JSON.stringify(current.datasetSource) : null
|
|
578
|
+
: params.datasetSource ? JSON.stringify(params.datasetSource) : null, nextVersion, now, params.panelId, current.version);
|
|
579
|
+
updateChanges = updateResult.changes;
|
|
580
|
+
if (updateChanges === 0)
|
|
581
|
+
return;
|
|
582
|
+
if (params.rows) {
|
|
583
|
+
replacePanelRows(db, params.panelId, nextRows);
|
|
584
|
+
remapPanelSelectionState(db, params.panelId, currentRows, nextRows, now);
|
|
585
|
+
}
|
|
586
|
+
})();
|
|
587
|
+
if (updateChanges === 0) {
|
|
588
|
+
const latest = getPanelById(db, params.panelId);
|
|
589
|
+
if (latest?.archivedAt) {
|
|
590
|
+
return { ok: false, failureClass: 'panel_archived', details: { panelId: params.panelId } };
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
failureClass: 'version_conflict',
|
|
595
|
+
details: {
|
|
596
|
+
panelId: params.panelId,
|
|
597
|
+
expectedVersion: current.version,
|
|
598
|
+
currentVersion: getPanelById(db, params.panelId)?.version ?? current.version,
|
|
599
|
+
recommendedAction: 'Read the latest panel state/events/rows, merge user changes, then retry with the current version. Do not blindly overwrite saved user state or annotations.',
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const panel = getPanelById(db, params.panelId);
|
|
604
|
+
if (!panel) {
|
|
605
|
+
return { ok: false, failureClass: 'patch_failed', details: { panelId: params.panelId } };
|
|
606
|
+
}
|
|
607
|
+
return { ok: true, panel, changed: Array.from(changed) };
|
|
608
|
+
}
|
|
609
|
+
export function listAllPanelRows(db, panelId) {
|
|
610
|
+
const rows = db.prepare(`SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson,
|
|
611
|
+
node_id as nodeId, cached_asset_ids_json as cachedAssetIdsJson
|
|
612
|
+
FROM panel_rows
|
|
613
|
+
WHERE panel_id = ?
|
|
614
|
+
ORDER BY row_index ASC`).all(panelId);
|
|
615
|
+
return rows.map((row) => ({
|
|
616
|
+
rowIndex: row.rowIndex,
|
|
617
|
+
rowId: row.rowId,
|
|
618
|
+
fields: parseJsonObject(row.fieldsJson),
|
|
619
|
+
media: parseJsonObject(row.mediaJson),
|
|
620
|
+
nodeId: row.nodeId,
|
|
621
|
+
cachedAssetIds: parseJsonObject(row.cachedAssetIdsJson),
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
function applyRowPatch(db, panelId, currentRows, patch, defaultNodeId) {
|
|
625
|
+
let rows = currentRows.map((row) => ({ ...row, fields: { ...row.fields }, media: { ...row.media } }));
|
|
626
|
+
const byRowId = new Map();
|
|
627
|
+
for (const row of rows) {
|
|
628
|
+
if (row.rowId)
|
|
629
|
+
byRowId.set(row.rowId, row.rowIndex);
|
|
630
|
+
}
|
|
631
|
+
const removeIds = Array.from(new Set((patch.removeRowIds ?? []).map((id) => id.trim()).filter(Boolean)));
|
|
632
|
+
if (removeIds.length) {
|
|
633
|
+
const missing = removeIds.filter((rowId) => !byRowId.has(rowId));
|
|
634
|
+
if (missing.length) {
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
failureClass: 'row_not_found',
|
|
638
|
+
details: { panelId, rowIds: missing },
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const removeSet = new Set(removeIds);
|
|
642
|
+
rows = rows.filter((row) => !row.rowId || !removeSet.has(row.rowId));
|
|
643
|
+
byRowId.clear();
|
|
644
|
+
rows.forEach((row, index) => {
|
|
645
|
+
if (row.rowId)
|
|
646
|
+
byRowId.set(row.rowId, index);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const appendRows = patch.append ?? [];
|
|
650
|
+
for (const input of appendRows) {
|
|
651
|
+
const rowId = normalizePatchRowId(input.rowId)
|
|
652
|
+
?? generatePanelRowId(db, panelId, rows.length);
|
|
653
|
+
if (byRowId.has(rowId)) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
failureClass: 'duplicate_row_id',
|
|
657
|
+
details: { panelId, rowId, issue: 'append rowId already exists' },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
rows.push(toPanelRowMedia(panelId, rows.length, rowId, input, defaultNodeId));
|
|
661
|
+
byRowId.set(rowId, rows.length - 1);
|
|
662
|
+
}
|
|
663
|
+
const upsertRows = patch.upsert ?? [];
|
|
664
|
+
for (const input of upsertRows) {
|
|
665
|
+
const rowId = normalizePatchRowId(input.rowId);
|
|
666
|
+
if (!rowId) {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
failureClass: 'missing_row_id',
|
|
670
|
+
details: { panelId, issue: 'rows.upsert entries require rowId.' },
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const existingIndex = byRowId.get(rowId);
|
|
674
|
+
if (existingIndex === undefined) {
|
|
675
|
+
rows.push(toPanelRowMedia(panelId, rows.length, rowId, input, defaultNodeId));
|
|
676
|
+
byRowId.set(rowId, rows.length - 1);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const existing = rows[existingIndex];
|
|
680
|
+
const nextMedia = { ...existing.media, ...(input.media ?? {}) };
|
|
681
|
+
const nextCachedAssetIds = { ...existing.cachedAssetIds };
|
|
682
|
+
for (const [slot, media] of Object.entries(input.media ?? {})) {
|
|
683
|
+
const previous = existing.media[slot];
|
|
684
|
+
if (!previous || previous.kind !== media.kind || previous.value !== media.value) {
|
|
685
|
+
delete nextCachedAssetIds[slot];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
rows[existingIndex] = {
|
|
689
|
+
...existing,
|
|
690
|
+
fields: { ...existing.fields, ...input.fields },
|
|
691
|
+
media: nextMedia,
|
|
692
|
+
cachedAssetIds: nextCachedAssetIds,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
rows: rows.map((row, index) => ({ ...row, rowIndex: index })),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function normalizePatchRowId(rowId) {
|
|
701
|
+
if (typeof rowId !== 'string')
|
|
702
|
+
return null;
|
|
703
|
+
const trimmed = rowId.trim();
|
|
704
|
+
return trimmed || null;
|
|
705
|
+
}
|
|
706
|
+
function toPanelRowMedia(panelId, rowIndex, rowId, row, defaultNodeId) {
|
|
707
|
+
return {
|
|
708
|
+
rowIndex,
|
|
709
|
+
rowId,
|
|
710
|
+
fields: { ...row.fields },
|
|
711
|
+
media: { ...(row.media ?? {}) },
|
|
712
|
+
nodeId: defaultNodeId,
|
|
713
|
+
cachedAssetIds: {},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function replacePanelRows(db, panelId, rows) {
|
|
717
|
+
db.prepare('DELETE FROM panel_rows WHERE panel_id = ?').run(panelId);
|
|
718
|
+
insertPanelRows(db, rows.map((row, index) => ({
|
|
719
|
+
panelId,
|
|
720
|
+
rowIndex: index,
|
|
721
|
+
rowId: row.rowId,
|
|
722
|
+
fields: row.fields,
|
|
723
|
+
media: row.media,
|
|
724
|
+
nodeId: row.nodeId,
|
|
725
|
+
cachedAssetIds: row.cachedAssetIds,
|
|
726
|
+
})));
|
|
727
|
+
}
|
|
728
|
+
function remapPanelSelectionState(db, panelId, oldRows, newRows, now) {
|
|
729
|
+
const oldIndexToRowId = new Map();
|
|
730
|
+
for (const row of oldRows) {
|
|
731
|
+
if (row.rowId)
|
|
732
|
+
oldIndexToRowId.set(row.rowIndex, row.rowId);
|
|
733
|
+
}
|
|
734
|
+
const rowIdToNewIndex = new Map();
|
|
735
|
+
for (const row of newRows) {
|
|
736
|
+
if (row.rowId)
|
|
737
|
+
rowIdToNewIndex.set(row.rowId, row.rowIndex);
|
|
738
|
+
}
|
|
739
|
+
const remapSelection = (selection) => {
|
|
740
|
+
if (!Array.isArray(selection))
|
|
741
|
+
return [];
|
|
742
|
+
const out = new Set();
|
|
743
|
+
for (const value of selection) {
|
|
744
|
+
if (!Number.isInteger(value) || value < 0)
|
|
745
|
+
continue;
|
|
746
|
+
const rowId = oldIndexToRowId.get(value);
|
|
747
|
+
const nextIndex = rowId ? rowIdToNewIndex.get(rowId) : undefined;
|
|
748
|
+
if (nextIndex !== undefined)
|
|
749
|
+
out.add(nextIndex);
|
|
750
|
+
}
|
|
751
|
+
return Array.from(out).sort((a, b) => a - b);
|
|
752
|
+
};
|
|
753
|
+
const panelStateRows = db.prepare(`SELECT user_id as userId, state_json as stateJson FROM panel_state WHERE panel_id = ?`).all(panelId);
|
|
754
|
+
const updatePanelState = db.prepare(`UPDATE panel_state SET state_json = ?, updated_at = ? WHERE panel_id = ? AND user_id = ?`);
|
|
755
|
+
for (const row of panelStateRows) {
|
|
756
|
+
const state = parseJsonObject(row.stateJson);
|
|
757
|
+
const nextSelection = remapSelection(state.selection);
|
|
758
|
+
if (JSON.stringify(state.selection ?? []) === JSON.stringify(nextSelection))
|
|
759
|
+
continue;
|
|
760
|
+
updatePanelState.run(JSON.stringify({ ...state, selection: nextSelection }), now, panelId, row.userId);
|
|
761
|
+
}
|
|
762
|
+
const shared = db.prepare(`SELECT state_json as stateJson, version FROM panel_shared_state WHERE panel_id = ? LIMIT 1`).get(panelId);
|
|
763
|
+
if (!shared)
|
|
764
|
+
return;
|
|
765
|
+
const sharedState = parseJsonObject(shared.stateJson);
|
|
766
|
+
const nextSharedSelection = remapSelection(sharedState.selection);
|
|
767
|
+
if (JSON.stringify(sharedState.selection ?? []) === JSON.stringify(nextSharedSelection))
|
|
768
|
+
return;
|
|
769
|
+
db.prepare(`UPDATE panel_shared_state
|
|
770
|
+
SET state_json = ?, version = ?, updated_at = ?
|
|
771
|
+
WHERE panel_id = ?`).run(JSON.stringify({ ...sharedState, selection: nextSharedSelection }), shared.version + 1, now, panelId);
|
|
772
|
+
}
|
|
773
|
+
export function updatePanelRowCachedAssetId(db, panelId, rowIndex, slot, assetId) {
|
|
774
|
+
const row = db.prepare(`SELECT cached_asset_ids_json FROM panel_rows WHERE panel_id = ? AND row_index = ?`).get(panelId, rowIndex);
|
|
775
|
+
const current = parseJsonObject(row?.cached_asset_ids_json ?? '{}');
|
|
776
|
+
current[slot] = assetId;
|
|
777
|
+
db.prepare(`UPDATE panel_rows SET cached_asset_ids_json = ? WHERE panel_id = ? AND row_index = ?`).run(JSON.stringify(current), panelId, rowIndex);
|
|
778
|
+
}
|
|
779
|
+
export function updatePanelRowCachedAssetIdIfCurrent(db, params) {
|
|
780
|
+
const row = db.prepare(`SELECT pr.row_id as rowId,
|
|
781
|
+
pr.node_id as nodeId,
|
|
782
|
+
pr.media_json as mediaJson,
|
|
783
|
+
pr.cached_asset_ids_json as cachedAssetIdsJson,
|
|
784
|
+
p.version as panelVersion
|
|
785
|
+
FROM panel_rows pr
|
|
786
|
+
JOIN panels p ON p.id = pr.panel_id
|
|
787
|
+
WHERE pr.panel_id = ? AND pr.row_index = ?
|
|
788
|
+
LIMIT 1`).get(params.panelId, params.rowIndex);
|
|
789
|
+
if (!row)
|
|
790
|
+
return false;
|
|
791
|
+
if (row.panelVersion !== params.panelVersion)
|
|
792
|
+
return false;
|
|
793
|
+
if (row.rowId !== params.rowId)
|
|
794
|
+
return false;
|
|
795
|
+
if (row.nodeId !== params.nodeId)
|
|
796
|
+
return false;
|
|
797
|
+
const media = parseJsonObject(row.mediaJson);
|
|
798
|
+
const currentMediaSlot = media[params.slot];
|
|
799
|
+
if (!currentMediaSlot
|
|
800
|
+
|| currentMediaSlot.kind !== params.mediaSlot.kind
|
|
801
|
+
|| currentMediaSlot.value !== params.mediaSlot.value) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
const current = parseJsonObject(row.cachedAssetIdsJson);
|
|
805
|
+
if ((current[params.slot] ?? null) !== (params.expectedCachedAssetId ?? null))
|
|
806
|
+
return false;
|
|
807
|
+
current[params.slot] = params.assetId;
|
|
808
|
+
const result = db.prepare(`UPDATE panel_rows SET cached_asset_ids_json = ? WHERE panel_id = ? AND row_index = ?`).run(JSON.stringify(current), params.panelId, params.rowIndex);
|
|
809
|
+
return result.changes > 0;
|
|
810
|
+
}
|
|
811
|
+
export function normalizePanelSelectionForRowCount(rowCount, selection) {
|
|
812
|
+
if (!Array.isArray(selection))
|
|
813
|
+
return [];
|
|
814
|
+
const selected = new Set();
|
|
815
|
+
for (const value of selection) {
|
|
816
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0)
|
|
817
|
+
continue;
|
|
818
|
+
if (Number.isInteger(rowCount) && rowCount != null && rowCount >= 0 && value >= rowCount)
|
|
819
|
+
continue;
|
|
820
|
+
selected.add(value);
|
|
821
|
+
}
|
|
822
|
+
return Array.from(selected).sort((a, b) => a - b);
|
|
823
|
+
}
|
|
824
|
+
export function normalizePanelStateSelectionForRowCount(rowCount, state) {
|
|
825
|
+
if (!Object.prototype.hasOwnProperty.call(state, 'selection'))
|
|
826
|
+
return state;
|
|
827
|
+
if (!Array.isArray(state.selection))
|
|
828
|
+
return { ...state, selection: [] };
|
|
829
|
+
const originalSelection = state.selection;
|
|
830
|
+
const selection = normalizePanelSelectionForRowCount(rowCount, state.selection);
|
|
831
|
+
if (selection.length === originalSelection.length
|
|
832
|
+
&& selection.every((rowIndex, index) => rowIndex === originalSelection[index])) {
|
|
833
|
+
return state;
|
|
834
|
+
}
|
|
835
|
+
return { ...state, selection };
|
|
836
|
+
}
|
|
837
|
+
export function applySharedSelectionPatch(db, panelId, patch, updatedByUserId) {
|
|
838
|
+
const now = Date.now();
|
|
839
|
+
const current = getSharedPanelState(db, panelId);
|
|
840
|
+
const currentSelection = patch.baseSelection ?? (current?.state?.selection ?? []);
|
|
841
|
+
const currentSet = new Set(currentSelection);
|
|
842
|
+
const toggled = patch.selection ?? [];
|
|
843
|
+
for (const rowIndex of toggled) {
|
|
844
|
+
if (currentSet.has(rowIndex)) {
|
|
845
|
+
currentSet.delete(rowIndex);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
currentSet.add(rowIndex);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const newSelection = Array.from(currentSet).sort((a, b) => a - b);
|
|
852
|
+
const newState = { selection: newSelection };
|
|
853
|
+
const newVersion = (current?.version ?? 0) + 1;
|
|
854
|
+
db.prepare(`INSERT INTO panel_shared_state(panel_id, state_json, version, updated_at, updated_by_user_id)
|
|
855
|
+
VALUES(?, ?, ?, ?, ?)
|
|
856
|
+
ON CONFLICT(panel_id)
|
|
857
|
+
DO UPDATE SET
|
|
858
|
+
state_json = excluded.state_json,
|
|
859
|
+
version = excluded.version,
|
|
860
|
+
updated_at = excluded.updated_at,
|
|
861
|
+
updated_by_user_id = excluded.updated_by_user_id`).run(panelId, JSON.stringify(newState), newVersion, now, updatedByUserId ?? null);
|
|
862
|
+
return {
|
|
863
|
+
panelId,
|
|
864
|
+
state: newState,
|
|
865
|
+
version: newVersion,
|
|
866
|
+
updatedAt: new Date(now).toISOString(),
|
|
867
|
+
...(updatedByUserId ? { updatedByUserId } : {}),
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
export function getSharedPanelState(db, panelId) {
|
|
871
|
+
const row = db.prepare(`SELECT panel_id as panelId, state_json as stateJson, version,
|
|
872
|
+
updated_at as updatedAt, updated_by_user_id as updatedByUserId
|
|
873
|
+
FROM panel_shared_state
|
|
874
|
+
WHERE panel_id = ?
|
|
875
|
+
LIMIT 1`).get(panelId);
|
|
876
|
+
if (!row)
|
|
877
|
+
return null;
|
|
878
|
+
return {
|
|
879
|
+
panelId: row.panelId,
|
|
880
|
+
state: parseJsonObject(row.stateJson),
|
|
881
|
+
version: row.version,
|
|
882
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
883
|
+
...(row.updatedByUserId ? { updatedByUserId: row.updatedByUserId } : {}),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
export function upsertSharedPanelState(db, panelId, state, updatedByUserId) {
|
|
887
|
+
const now = Date.now();
|
|
888
|
+
db.prepare(`INSERT INTO panel_shared_state(panel_id, state_json, version, updated_at, updated_by_user_id)
|
|
889
|
+
VALUES(?, ?, ?, ?, ?)
|
|
890
|
+
ON CONFLICT(panel_id)
|
|
891
|
+
DO UPDATE SET
|
|
892
|
+
state_json = excluded.state_json,
|
|
893
|
+
version = excluded.version,
|
|
894
|
+
updated_at = excluded.updated_at,
|
|
895
|
+
updated_by_user_id = excluded.updated_by_user_id`).run(panelId, JSON.stringify(state), 1, now, updatedByUserId ?? null);
|
|
896
|
+
}
|
|
897
|
+
export function loadAttachmentRow(db, id) {
|
|
898
|
+
const row = db.prepare(`SELECT id, filename, original_filename as originalFilename, mime_type as mimeType,
|
|
899
|
+
size_bytes as sizeBytes, storage_path as storagePath, kind
|
|
900
|
+
FROM attachments
|
|
901
|
+
WHERE id = ?
|
|
902
|
+
LIMIT 1`).get(id);
|
|
903
|
+
return row ?? null;
|
|
904
|
+
}
|