forge-openclaw-plugin 0.2.19 → 0.2.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 +133 -2
- package/dist/assets/board-_C6oMy5w.js +6 -0
- package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.css +1 -0
- package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
- package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-DT3pnAKJ.css +1 -0
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/viz-C6hfyqzu.js +34 -0
- package/dist/assets/viz-C6hfyqzu.js.map +1 -0
- package/dist/index.html +9 -9
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -2
- package/dist/openclaw/routes.js +207 -24
- package/dist/openclaw/tools.js +324 -35
- package/dist/server/app.js +2080 -92
- package/dist/server/db.js +3 -0
- package/dist/server/health.js +1284 -0
- package/dist/server/managers/platform/background-job-manager.js +138 -2
- package/dist/server/managers/platform/llm-manager.js +126 -0
- package/dist/server/managers/platform/openai-responses-provider.js +773 -0
- package/dist/server/managers/runtime.js +6 -1
- package/dist/server/openapi.js +718 -0
- package/dist/server/preferences-seeds.js +409 -0
- package/dist/server/preferences-types.js +368 -0
- package/dist/server/psyche-types.js +42 -18
- package/dist/server/repositories/activity-events.js +53 -4
- package/dist/server/repositories/calendar.js +89 -15
- package/dist/server/repositories/collaboration.js +8 -3
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/entity-ownership.js +92 -0
- package/dist/server/repositories/goals.js +7 -2
- package/dist/server/repositories/habits.js +122 -16
- package/dist/server/repositories/notes.js +119 -41
- package/dist/server/repositories/preferences.js +1765 -0
- package/dist/server/repositories/projects.js +18 -7
- package/dist/server/repositories/psyche.js +84 -27
- package/dist/server/repositories/rewards.js +112 -4
- package/dist/server/repositories/strategies.js +450 -0
- package/dist/server/repositories/tags.js +11 -6
- package/dist/server/repositories/task-runs.js +10 -2
- package/dist/server/repositories/tasks.js +99 -17
- package/dist/server/repositories/users.js +417 -0
- package/dist/server/repositories/wiki-memory.js +3366 -0
- package/dist/server/services/context.js +20 -18
- package/dist/server/services/dashboard.js +29 -6
- package/dist/server/services/entity-crud.js +21 -3
- package/dist/server/services/insights.js +9 -7
- package/dist/server/services/projects.js +2 -1
- package/dist/server/services/psyche.js +10 -9
- package/dist/server/types.js +594 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/server/migrations/016_health_companion.sql +158 -0
- package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/server/migrations/017_preferences.sql +131 -0
- package/server/migrations/018_preference_catalogs.sql +31 -0
- package/server/migrations/019_wiki_memory.sql +255 -0
- package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +126 -34
- package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
- package/dist/assets/board-8L3uX7_O.js +0 -6
- package/dist/assets/index-Cj1IBH_w.js +0 -36
- package/dist/assets/index-Cj1IBH_w.js.map +0 -1
- package/dist/assets/index-DQT6EbuS.css +0 -1
- package/dist/assets/vendor-BvM2F9Dp.js +0 -503
- package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
- package/dist/assets/vendor-CRS-psbw.css +0 -1
- package/dist/assets/viz-CNeunkfu.js +0 -34
- package/dist/assets/viz-CNeunkfu.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
3
|
import { recordActivityEvent } from "./activity-events.js";
|
|
4
|
+
import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
|
|
4
5
|
import { getProjectById } from "./projects.js";
|
|
5
6
|
import { getTaskById } from "./tasks.js";
|
|
6
7
|
import { calendarConnectionSchema, calendarContextConflictSchema, calendarEventSchema, calendarEventLinkSchema, calendarEventSourceSchema, calendarOverviewPayloadSchema, calendarSchema, calendarSchedulingRulesSchema, taskTimeboxSchema, workBlockInstanceSchema, workBlockTemplateSchema } from "../types.js";
|
|
@@ -130,6 +131,15 @@ function mapEvent(row) {
|
|
|
130
131
|
title: row.title,
|
|
131
132
|
description: row.description,
|
|
132
133
|
location: row.location,
|
|
134
|
+
place: {
|
|
135
|
+
label: row.place_label,
|
|
136
|
+
address: row.place_address,
|
|
137
|
+
timezone: row.place_timezone,
|
|
138
|
+
latitude: row.place_latitude,
|
|
139
|
+
longitude: row.place_longitude,
|
|
140
|
+
source: row.place_source,
|
|
141
|
+
externalPlaceId: row.place_external_id
|
|
142
|
+
},
|
|
133
143
|
startAt: row.start_at,
|
|
134
144
|
endAt: row.end_at,
|
|
135
145
|
timezone: normalizeTimezone(row.timezone),
|
|
@@ -181,6 +191,20 @@ function mapTimebox(row) {
|
|
|
181
191
|
updatedAt: row.updated_at
|
|
182
192
|
});
|
|
183
193
|
}
|
|
194
|
+
function inferCalendarEventOwnerId(input) {
|
|
195
|
+
return (input.userId ??
|
|
196
|
+
inferFirstOwnedUserId((input.links ?? []).map((link) => ({
|
|
197
|
+
entityType: link.entityType,
|
|
198
|
+
entityId: link.entityId
|
|
199
|
+
}))));
|
|
200
|
+
}
|
|
201
|
+
function inferTaskTimeboxOwnerId(input) {
|
|
202
|
+
return (input.userId ??
|
|
203
|
+
inferFirstOwnedUserId([
|
|
204
|
+
{ entityType: "task", entityId: input.taskId },
|
|
205
|
+
{ entityType: "project", entityId: input.projectId ?? null }
|
|
206
|
+
]));
|
|
207
|
+
}
|
|
184
208
|
function addMinutes(date, minutes) {
|
|
185
209
|
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
186
210
|
}
|
|
@@ -385,25 +409,28 @@ export function listCalendarEvents(query) {
|
|
|
385
409
|
params.push(query.to);
|
|
386
410
|
const rows = getDatabase()
|
|
387
411
|
.prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
|
|
412
|
+
place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
|
|
388
413
|
start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
|
|
389
414
|
FROM forge_events
|
|
390
415
|
WHERE ${clauses.join(" AND ")}
|
|
391
416
|
ORDER BY start_at ASC, title ASC`)
|
|
392
417
|
.all(...params);
|
|
393
|
-
return rows.map(mapEvent);
|
|
418
|
+
return filterOwnedEntities("calendar_event", rows.map(mapEvent), query.userIds);
|
|
394
419
|
}
|
|
395
420
|
export function getCalendarEventById(eventId) {
|
|
396
421
|
const row = getDatabase()
|
|
397
422
|
.prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
|
|
423
|
+
place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
|
|
398
424
|
start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
|
|
399
425
|
FROM forge_events
|
|
400
426
|
WHERE id = ?`)
|
|
401
427
|
.get(eventId);
|
|
402
|
-
return row ? mapEvent(row) : undefined;
|
|
428
|
+
return row ? decorateOwnedEntity("calendar_event", mapEvent(row)) : undefined;
|
|
403
429
|
}
|
|
404
430
|
export function getCalendarEventStorageRecord(eventId) {
|
|
405
431
|
return getDatabase()
|
|
406
432
|
.prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
|
|
433
|
+
place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
|
|
407
434
|
start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
|
|
408
435
|
FROM forge_events
|
|
409
436
|
WHERE id = ?`)
|
|
@@ -413,6 +440,8 @@ export function getCalendarEventByRemoteId(connectionId, calendarId, remoteId) {
|
|
|
413
440
|
const row = getDatabase()
|
|
414
441
|
.prepare(`SELECT forge_events.id, forge_events.preferred_connection_id, forge_events.preferred_calendar_id, forge_events.ownership,
|
|
415
442
|
forge_events.origin_type, forge_events.status, forge_events.title, forge_events.description, forge_events.location,
|
|
443
|
+
forge_events.place_label, forge_events.place_address, forge_events.place_timezone, forge_events.place_latitude,
|
|
444
|
+
forge_events.place_longitude, forge_events.place_source, forge_events.place_external_id,
|
|
416
445
|
forge_events.start_at, forge_events.end_at, forge_events.timezone, forge_events.is_all_day, forge_events.availability,
|
|
417
446
|
forge_events.event_type, forge_events.categories_json, forge_events.deleted_at, forge_events.created_at, forge_events.updated_at
|
|
418
447
|
FROM forge_event_sources
|
|
@@ -489,9 +518,10 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
489
518
|
getDatabase()
|
|
490
519
|
.prepare(`UPDATE forge_events
|
|
491
520
|
SET preferred_connection_id = ?, preferred_calendar_id = ?, ownership = ?, origin_type = ?, status = ?, title = ?, description = ?, location = ?,
|
|
521
|
+
place_label = ?, place_address = ?, place_timezone = ?, place_latitude = ?, place_longitude = ?, place_source = ?, place_external_id = ?,
|
|
492
522
|
start_at = ?, end_at = ?, timezone = ?, is_all_day = ?, availability = ?, event_type = ?, categories_json = ?, deleted_at = ?, updated_at = ?
|
|
493
523
|
WHERE id = ?`)
|
|
494
|
-
.run(connectionId, calendar.id, input.ownership ?? existing.ownership, connection.provider, input.status ?? existing.status, input.title, input.description ?? "", input.location ?? "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? existing.availability, input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, existing.id);
|
|
524
|
+
.run(connectionId, calendar.id, input.ownership ?? existing.ownership, connection.provider, input.status ?? existing.status, input.title, input.description ?? "", input.location ?? "", input.location ?? "", "", "", null, null, "", "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? existing.availability, input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, existing.id);
|
|
495
525
|
upsertEventSource({
|
|
496
526
|
forgeEventId: existing.id,
|
|
497
527
|
provider: connection.provider,
|
|
@@ -516,10 +546,11 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
516
546
|
getDatabase()
|
|
517
547
|
.prepare(`INSERT INTO forge_events (
|
|
518
548
|
id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
|
|
549
|
+
place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
|
|
519
550
|
start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
|
|
520
551
|
)
|
|
521
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
522
|
-
.run(id, connectionId, calendar.id, input.ownership ?? "external", connection.provider, input.status ?? "confirmed", input.title, input.description ?? "", input.location ?? "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? "busy", input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, now);
|
|
552
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
553
|
+
.run(id, connectionId, calendar.id, input.ownership ?? "external", connection.provider, input.status ?? "confirmed", input.title, input.description ?? "", input.location ?? "", input.location ?? "", "", "", null, null, "", "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? "busy", input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, now);
|
|
523
554
|
upsertEventSource({
|
|
524
555
|
forgeEventId: id,
|
|
525
556
|
provider: connection.provider,
|
|
@@ -543,6 +574,15 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
543
574
|
export function createCalendarEvent(input) {
|
|
544
575
|
const now = nowIso();
|
|
545
576
|
const id = `calevent_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
577
|
+
const place = input.place ?? {
|
|
578
|
+
label: "",
|
|
579
|
+
address: "",
|
|
580
|
+
timezone: "",
|
|
581
|
+
latitude: null,
|
|
582
|
+
longitude: null,
|
|
583
|
+
source: "",
|
|
584
|
+
externalPlaceId: ""
|
|
585
|
+
};
|
|
546
586
|
const preferredCalendar = input.preferredCalendarId === undefined
|
|
547
587
|
? getDefaultWritableCalendar() ?? null
|
|
548
588
|
: input.preferredCalendarId
|
|
@@ -551,11 +591,13 @@ export function createCalendarEvent(input) {
|
|
|
551
591
|
getDatabase()
|
|
552
592
|
.prepare(`INSERT INTO forge_events (
|
|
553
593
|
id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
|
|
594
|
+
place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
|
|
554
595
|
start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, created_at, updated_at
|
|
555
596
|
)
|
|
556
|
-
VALUES (?, ?, ?,
|
|
557
|
-
.run(id, preferredCalendar?.connectionId ?? null, preferredCalendar?.id ?? null, input.title, input.description, input.location, input.startAt, input.endAt, normalizeTimezone(input.timezone), input.isAllDay ? 1 : 0, input.availability, input.eventType, JSON.stringify(input.categories), now, now);
|
|
597
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
598
|
+
.run(id, preferredCalendar?.connectionId ?? null, preferredCalendar?.id ?? null, "forge", "native", "confirmed", input.title, input.description, input.location, place.label || input.location, place.address, place.timezone, place.latitude, place.longitude, place.source, place.externalPlaceId, input.startAt, input.endAt, normalizeTimezone(input.timezone), input.isAllDay ? 1 : 0, input.availability, input.eventType, JSON.stringify(input.categories), now, now);
|
|
558
599
|
replaceEventLinks(id, input.links);
|
|
600
|
+
setEntityOwner("calendar_event", id, inferCalendarEventOwnerId(input));
|
|
559
601
|
return getCalendarEventById(id);
|
|
560
602
|
}
|
|
561
603
|
export function updateCalendarEvent(eventId, patch) {
|
|
@@ -578,6 +620,19 @@ export function updateCalendarEvent(eventId, patch) {
|
|
|
578
620
|
title: patch.title ?? current.title,
|
|
579
621
|
description: patch.description ?? current.description,
|
|
580
622
|
location: patch.location ?? current.location,
|
|
623
|
+
place: {
|
|
624
|
+
label: patch.place?.label ?? current.place.label,
|
|
625
|
+
address: patch.place?.address ?? current.place.address,
|
|
626
|
+
timezone: patch.place?.timezone ?? current.place.timezone,
|
|
627
|
+
latitude: patch.place?.latitude === undefined
|
|
628
|
+
? current.place.latitude
|
|
629
|
+
: patch.place.latitude,
|
|
630
|
+
longitude: patch.place?.longitude === undefined
|
|
631
|
+
? current.place.longitude
|
|
632
|
+
: patch.place.longitude,
|
|
633
|
+
source: patch.place?.source ?? current.place.source,
|
|
634
|
+
externalPlaceId: patch.place?.externalPlaceId ?? current.place.externalPlaceId
|
|
635
|
+
},
|
|
581
636
|
startAt: patch.startAt ?? current.startAt,
|
|
582
637
|
endAt: patch.endAt ?? current.endAt,
|
|
583
638
|
timezone: normalizeTimezone(patch.timezone ?? current.timezone),
|
|
@@ -590,12 +645,21 @@ export function updateCalendarEvent(eventId, patch) {
|
|
|
590
645
|
getDatabase()
|
|
591
646
|
.prepare(`UPDATE forge_events
|
|
592
647
|
SET preferred_connection_id = ?, preferred_calendar_id = ?, title = ?, description = ?, location = ?,
|
|
648
|
+
place_label = ?, place_address = ?, place_timezone = ?, place_latitude = ?, place_longitude = ?, place_source = ?, place_external_id = ?,
|
|
593
649
|
start_at = ?, end_at = ?, timezone = ?, is_all_day = ?, availability = ?, event_type = ?, categories_json = ?, updated_at = ?
|
|
594
650
|
WHERE id = ?`)
|
|
595
|
-
.run(next.preferredConnectionId, next.preferredCalendarId, next.title, next.description, next.location, next.startAt, next.endAt, next.timezone, next.isAllDay ? 1 : 0, next.availability, next.eventType, JSON.stringify(next.categories), next.updatedAt, eventId);
|
|
651
|
+
.run(next.preferredConnectionId, next.preferredCalendarId, next.title, next.description, next.location, next.place.label, next.place.address, next.place.timezone, next.place.latitude, next.place.longitude, next.place.source, next.place.externalPlaceId, next.startAt, next.endAt, next.timezone, next.isAllDay ? 1 : 0, next.availability, next.eventType, JSON.stringify(next.categories), next.updatedAt, eventId);
|
|
596
652
|
if (patch.links) {
|
|
597
653
|
replaceEventLinks(eventId, patch.links);
|
|
598
654
|
}
|
|
655
|
+
if (patch.userId !== undefined || patch.links !== undefined) {
|
|
656
|
+
setEntityOwner("calendar_event", eventId, patch.userId === undefined
|
|
657
|
+
? inferCalendarEventOwnerId({
|
|
658
|
+
userId: current.userId ?? null,
|
|
659
|
+
links: patch.links ?? current.links
|
|
660
|
+
})
|
|
661
|
+
: patch.userId);
|
|
662
|
+
}
|
|
599
663
|
if (current.sourceMappings.length > 0) {
|
|
600
664
|
const nextSyncState = current.deletedAt !== null ? "deleted" : current.originType === "native" ? "pending_update" : "synced";
|
|
601
665
|
getDatabase()
|
|
@@ -635,16 +699,17 @@ export function createWorkBlockTemplate(input) {
|
|
|
635
699
|
)
|
|
636
700
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
637
701
|
.run(id, input.title, input.kind, input.color, normalizeTimezone(input.timezone), JSON.stringify(input.weekDays), input.startMinute, input.endMinute, input.startsOn ?? null, input.endsOn ?? null, input.blockingState, now, now);
|
|
702
|
+
setEntityOwner("work_block_template", id, input.userId);
|
|
638
703
|
return getWorkBlockTemplateById(id);
|
|
639
704
|
});
|
|
640
705
|
}
|
|
641
|
-
export function listWorkBlockTemplates() {
|
|
706
|
+
export function listWorkBlockTemplates(filters = {}) {
|
|
642
707
|
const rows = getDatabase()
|
|
643
708
|
.prepare(`SELECT id, title, kind, color, timezone, weekdays_json, start_minute, end_minute, starts_on, ends_on, blocking_state, created_at, updated_at
|
|
644
709
|
FROM work_block_templates
|
|
645
710
|
ORDER BY COALESCE(starts_on, ''), start_minute ASC, title ASC`)
|
|
646
711
|
.all();
|
|
647
|
-
return rows.map(mapWorkBlockTemplate);
|
|
712
|
+
return filterOwnedEntities("work_block_template", rows.map(mapWorkBlockTemplate), filters.userIds);
|
|
648
713
|
}
|
|
649
714
|
export function getWorkBlockTemplateById(templateId) {
|
|
650
715
|
const row = getDatabase()
|
|
@@ -652,7 +717,9 @@ export function getWorkBlockTemplateById(templateId) {
|
|
|
652
717
|
FROM work_block_templates
|
|
653
718
|
WHERE id = ?`)
|
|
654
719
|
.get(templateId);
|
|
655
|
-
return row
|
|
720
|
+
return row
|
|
721
|
+
? decorateOwnedEntity("work_block_template", mapWorkBlockTemplate(row))
|
|
722
|
+
: undefined;
|
|
656
723
|
}
|
|
657
724
|
export function updateWorkBlockTemplate(templateId, patch) {
|
|
658
725
|
const current = getWorkBlockTemplateById(templateId);
|
|
@@ -677,6 +744,9 @@ export function updateWorkBlockTemplate(templateId, patch) {
|
|
|
677
744
|
SET title = ?, kind = ?, color = ?, timezone = ?, weekdays_json = ?, start_minute = ?, end_minute = ?, starts_on = ?, ends_on = ?, blocking_state = ?, updated_at = ?
|
|
678
745
|
WHERE id = ?`)
|
|
679
746
|
.run(next.title, next.kind, next.color, next.timezone, JSON.stringify(next.weekDays), next.startMinute, next.endMinute, next.startsOn, next.endsOn, next.blockingState, next.updatedAt, templateId);
|
|
747
|
+
if (patch.userId !== undefined) {
|
|
748
|
+
setEntityOwner("work_block_template", templateId, patch.userId);
|
|
749
|
+
}
|
|
680
750
|
return getWorkBlockTemplateById(templateId);
|
|
681
751
|
}
|
|
682
752
|
export function deleteWorkBlockTemplate(templateId) {
|
|
@@ -730,7 +800,7 @@ export function ensureWorkBlockInstancesInRange(_query) {
|
|
|
730
800
|
return [];
|
|
731
801
|
}
|
|
732
802
|
export function listWorkBlockInstances(query) {
|
|
733
|
-
return listWorkBlockTemplates()
|
|
803
|
+
return listWorkBlockTemplates({ userIds: query.userIds })
|
|
734
804
|
.flatMap((template) => deriveWorkBlockInstances(template, query))
|
|
735
805
|
.sort((left, right) => left.startAt.localeCompare(right.startAt) || left.title.localeCompare(right.title));
|
|
736
806
|
}
|
|
@@ -752,7 +822,7 @@ export function listTaskTimeboxes(query) {
|
|
|
752
822
|
WHERE ${clauses.join(" AND ")}
|
|
753
823
|
ORDER BY starts_at ASC`)
|
|
754
824
|
.all(...params);
|
|
755
|
-
return rows.map(mapTimebox);
|
|
825
|
+
return filterOwnedEntities("task_timebox", rows.map(mapTimebox), query.userIds);
|
|
756
826
|
}
|
|
757
827
|
export function getTaskTimeboxById(timeboxId) {
|
|
758
828
|
const row = getDatabase()
|
|
@@ -761,7 +831,7 @@ export function getTaskTimeboxById(timeboxId) {
|
|
|
761
831
|
FROM task_timeboxes
|
|
762
832
|
WHERE id = ?`)
|
|
763
833
|
.get(timeboxId);
|
|
764
|
-
return row ? mapTimebox(row) : undefined;
|
|
834
|
+
return row ? decorateOwnedEntity("task_timebox", mapTimebox(row)) : undefined;
|
|
765
835
|
}
|
|
766
836
|
export function createTaskTimebox(input) {
|
|
767
837
|
const now = nowIso();
|
|
@@ -772,6 +842,7 @@ export function createTaskTimebox(input) {
|
|
|
772
842
|
)
|
|
773
843
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
774
844
|
.run(id, input.taskId, input.projectId ?? null, input.connectionId ?? null, input.calendarId ?? null, input.linkedTaskRunId ?? null, input.status ?? "planned", input.source ?? "manual", input.title, input.startsAt, input.endsAt, input.overrideReason ?? null, now, now);
|
|
845
|
+
setEntityOwner("task_timebox", id, inferTaskTimeboxOwnerId(input));
|
|
775
846
|
return getTaskTimeboxById(id);
|
|
776
847
|
}
|
|
777
848
|
export function updateTaskTimebox(timeboxId, patch) {
|
|
@@ -798,6 +869,9 @@ export function updateTaskTimebox(timeboxId, patch) {
|
|
|
798
869
|
starts_at = ?, ends_at = ?, override_reason = ?, updated_at = ?
|
|
799
870
|
WHERE id = ?`)
|
|
800
871
|
.run(next.connectionId, next.calendarId, next.remoteEventId, next.linkedTaskRunId, next.status, next.source, next.title, next.startsAt, next.endsAt, next.overrideReason, next.updatedAt, timeboxId);
|
|
872
|
+
if (patch.userId !== undefined) {
|
|
873
|
+
setEntityOwner("task_timebox", timeboxId, patch.userId);
|
|
874
|
+
}
|
|
801
875
|
return getTaskTimeboxById(timeboxId);
|
|
802
876
|
}
|
|
803
877
|
export function deleteTaskTimebox(timeboxId) {
|
|
@@ -1082,7 +1156,7 @@ export function getCalendarOverview(query) {
|
|
|
1082
1156
|
connections: listCalendarConnections().map(({ credentialsSecretId: _secret, ...connection }) => connection),
|
|
1083
1157
|
calendars: listCalendars(),
|
|
1084
1158
|
events: listCalendarEvents(query),
|
|
1085
|
-
workBlockTemplates: listWorkBlockTemplates(),
|
|
1159
|
+
workBlockTemplates: listWorkBlockTemplates({ userIds: query.userIds }),
|
|
1086
1160
|
workBlockInstances: listWorkBlockInstances(query),
|
|
1087
1161
|
timeboxes: listTaskTimeboxes(query)
|
|
1088
1162
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase } from "../db.js";
|
|
3
|
+
import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
|
|
3
4
|
import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
|
|
4
5
|
import { recordActivityEvent } from "./activity-events.js";
|
|
5
6
|
import { recordEventLog } from "./event-log.js";
|
|
@@ -8,7 +9,7 @@ import { createTask } from "./tasks.js";
|
|
|
8
9
|
import { recordInsightAppliedReward } from "./rewards.js";
|
|
9
10
|
import { agentActionSchema, approvalRequestSchema, createAgentActionSchema, createInsightFeedbackSchema, createInsightSchema, insightFeedbackSchema, insightSchema, updateInsightSchema } from "../types.js";
|
|
10
11
|
function mapInsight(row) {
|
|
11
|
-
return insightSchema.parse({
|
|
12
|
+
return insightSchema.parse(decorateOwnedEntity("insight", {
|
|
12
13
|
id: row.id,
|
|
13
14
|
originType: row.origin_type,
|
|
14
15
|
originAgentId: row.origin_agent_id,
|
|
@@ -27,7 +28,7 @@ function mapInsight(row) {
|
|
|
27
28
|
evidence: JSON.parse(row.evidence_json),
|
|
28
29
|
createdAt: row.created_at,
|
|
29
30
|
updatedAt: row.updated_at
|
|
30
|
-
});
|
|
31
|
+
}));
|
|
31
32
|
}
|
|
32
33
|
function mapFeedback(row) {
|
|
33
34
|
return insightFeedbackSchema.parse({
|
|
@@ -132,7 +133,7 @@ export function listInsights(filters = {}) {
|
|
|
132
133
|
ORDER BY created_at DESC
|
|
133
134
|
${limitSql}`)
|
|
134
135
|
.all(...params);
|
|
135
|
-
return filterDeletedEntities("insight", rows.map(mapInsight));
|
|
136
|
+
return filterDeletedEntities("insight", filterOwnedEntities("insight", rows.map(mapInsight), filters.userIds));
|
|
136
137
|
}
|
|
137
138
|
export function getInsightById(insightId) {
|
|
138
139
|
if (isEntityDeleted("insight", insightId)) {
|
|
@@ -151,6 +152,9 @@ export function createInsight(input, context) {
|
|
|
151
152
|
title, summary, recommendation, rationale, confidence, cta_label, evidence_json, created_at, updated_at
|
|
152
153
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
153
154
|
.run(insightId, parsed.originType, parsed.originAgentId, parsed.originLabel, parsed.visibility, parsed.status, parsed.entityType, parsed.entityId, parsed.timeframeLabel, parsed.title, parsed.summary, parsed.recommendation, parsed.rationale, parsed.confidence, parsed.ctaLabel, JSON.stringify(parsed.evidence), now, now);
|
|
155
|
+
setEntityOwner("insight", insightId, inferFirstOwnedUserId(parsed.entityType && parsed.entityId
|
|
156
|
+
? [{ entityType: parsed.entityType, entityId: parsed.entityId }]
|
|
157
|
+
: []), context.actor ?? parsed.originLabel ?? null);
|
|
154
158
|
recordActivityEvent({
|
|
155
159
|
entityType: "insight",
|
|
156
160
|
entityId: insightId,
|
|
@@ -191,6 +195,7 @@ export function updateInsight(insightId, input, context) {
|
|
|
191
195
|
recommendation = ?, rationale = ?, confidence = ?, cta_label = ?, evidence_json = ?, updated_at = ?
|
|
192
196
|
WHERE id = ?`)
|
|
193
197
|
.run(parsed.visibility ?? current.visibility, parsed.status ?? current.status, parsed.entityType === undefined ? current.entityType : parsed.entityType, parsed.entityId === undefined ? current.entityId : parsed.entityId, parsed.timeframeLabel === undefined ? current.timeframeLabel : parsed.timeframeLabel, parsed.title ?? current.title, parsed.summary ?? current.summary, parsed.recommendation ?? current.recommendation, parsed.rationale ?? current.rationale, parsed.confidence ?? current.confidence, parsed.ctaLabel ?? current.ctaLabel, JSON.stringify(parsed.evidence ?? current.evidence), updatedAt, insightId);
|
|
198
|
+
setEntityOwner("insight", insightId, current.userId, context.actor ?? current.originLabel ?? null);
|
|
194
199
|
recordEventLog({
|
|
195
200
|
eventKind: "insight.updated",
|
|
196
201
|
entityType: "insight",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { getDatabase } from "../db.js";
|
|
4
|
+
import { createDiagnosticLogSchema, diagnosticLogEntrySchema } from "../types.js";
|
|
5
|
+
const MAX_LOG_ENTRIES = 5_000;
|
|
6
|
+
export const DIAGNOSTIC_LOG_RETENTION_DAYS = 14;
|
|
7
|
+
export const DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS = 15 * 60 * 1000;
|
|
8
|
+
const MAX_STRING_LENGTH = 4_000;
|
|
9
|
+
const MAX_ARRAY_ITEMS = 24;
|
|
10
|
+
const MAX_OBJECT_KEYS = 40;
|
|
11
|
+
const MAX_DEPTH = 4;
|
|
12
|
+
let nextRetentionSweepAt = 0;
|
|
13
|
+
function nowIso() {
|
|
14
|
+
return new Date().toISOString();
|
|
15
|
+
}
|
|
16
|
+
function sanitizeDiagnosticValue(value, depth = 0) {
|
|
17
|
+
if (value === null ||
|
|
18
|
+
typeof value === "boolean" ||
|
|
19
|
+
typeof value === "number") {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return value.length > MAX_STRING_LENGTH
|
|
24
|
+
? `${value.slice(0, MAX_STRING_LENGTH)}…`
|
|
25
|
+
: value;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "bigint") {
|
|
28
|
+
return value.toString();
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
if (value instanceof Date) {
|
|
34
|
+
return value.toISOString();
|
|
35
|
+
}
|
|
36
|
+
if (value instanceof Error) {
|
|
37
|
+
return {
|
|
38
|
+
name: value.name,
|
|
39
|
+
message: value.message,
|
|
40
|
+
stack: typeof value.stack === "string"
|
|
41
|
+
? sanitizeDiagnosticValue(value.stack, depth + 1)
|
|
42
|
+
: null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (Buffer.isBuffer(value)) {
|
|
46
|
+
return {
|
|
47
|
+
type: "Buffer",
|
|
48
|
+
bytes: value.byteLength
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (depth >= MAX_DEPTH) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return `[Array(${value.length})]`;
|
|
54
|
+
}
|
|
55
|
+
return "[Object]";
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return value
|
|
59
|
+
.slice(0, MAX_ARRAY_ITEMS)
|
|
60
|
+
.map((entry) => sanitizeDiagnosticValue(entry, depth + 1));
|
|
61
|
+
}
|
|
62
|
+
if (value && typeof value === "object") {
|
|
63
|
+
const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
|
|
64
|
+
return Object.fromEntries(entries.map(([key, entry]) => [
|
|
65
|
+
key,
|
|
66
|
+
sanitizeDiagnosticValue(entry, depth + 1)
|
|
67
|
+
]));
|
|
68
|
+
}
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
|
+
function sanitizeDetails(details) {
|
|
72
|
+
if (!details) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
return Object.fromEntries(Object.entries(details).map(([key, value]) => [
|
|
76
|
+
key,
|
|
77
|
+
sanitizeDiagnosticValue(value)
|
|
78
|
+
]));
|
|
79
|
+
}
|
|
80
|
+
function mapRow(row) {
|
|
81
|
+
return diagnosticLogEntrySchema.parse({
|
|
82
|
+
id: row.id,
|
|
83
|
+
level: row.level,
|
|
84
|
+
source: row.source,
|
|
85
|
+
scope: row.scope,
|
|
86
|
+
eventKey: row.event_key,
|
|
87
|
+
message: row.message,
|
|
88
|
+
route: row.route,
|
|
89
|
+
functionName: row.function_name,
|
|
90
|
+
requestId: row.request_id,
|
|
91
|
+
entityType: row.entity_type,
|
|
92
|
+
entityId: row.entity_id,
|
|
93
|
+
jobId: row.job_id,
|
|
94
|
+
details: JSON.parse(row.details_json),
|
|
95
|
+
createdAt: row.created_at
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function pruneDiagnosticLogs() {
|
|
99
|
+
const expiredBefore = new Date(Date.now() - DIAGNOSTIC_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1_000).toISOString();
|
|
100
|
+
const expiredResult = getDatabase()
|
|
101
|
+
.prepare(`DELETE FROM diagnostic_logs
|
|
102
|
+
WHERE created_at < ?`)
|
|
103
|
+
.run(expiredBefore);
|
|
104
|
+
const overflowResult = getDatabase().prepare(`DELETE FROM diagnostic_logs
|
|
105
|
+
WHERE id IN (
|
|
106
|
+
SELECT id
|
|
107
|
+
FROM diagnostic_logs
|
|
108
|
+
ORDER BY created_at DESC
|
|
109
|
+
LIMIT -1 OFFSET ?
|
|
110
|
+
)`).run(MAX_LOG_ENTRIES);
|
|
111
|
+
return (expiredResult.changes ?? 0) + (overflowResult.changes ?? 0);
|
|
112
|
+
}
|
|
113
|
+
function checkpointDiagnosticLogStore() {
|
|
114
|
+
getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
115
|
+
}
|
|
116
|
+
export function enforceDiagnosticLogRetention(options = {}) {
|
|
117
|
+
const now = options.now ?? new Date();
|
|
118
|
+
const startedAt = now.getTime();
|
|
119
|
+
if (!options.force && startedAt < nextRetentionSweepAt) {
|
|
120
|
+
return { prunedCount: 0, ran: false };
|
|
121
|
+
}
|
|
122
|
+
nextRetentionSweepAt =
|
|
123
|
+
startedAt + DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS;
|
|
124
|
+
const prunedCount = pruneDiagnosticLogs();
|
|
125
|
+
if (prunedCount > 0) {
|
|
126
|
+
checkpointDiagnosticLogStore();
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
prunedCount,
|
|
130
|
+
ran: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function recordDiagnosticLog(input, now = new Date()) {
|
|
134
|
+
const parsed = createDiagnosticLogSchema.parse(input);
|
|
135
|
+
const entry = diagnosticLogEntrySchema.parse({
|
|
136
|
+
id: `diag_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
|
|
137
|
+
level: parsed.level,
|
|
138
|
+
source: parsed.source ?? "server",
|
|
139
|
+
scope: parsed.scope,
|
|
140
|
+
eventKey: parsed.eventKey,
|
|
141
|
+
message: parsed.message,
|
|
142
|
+
route: parsed.route ?? null,
|
|
143
|
+
functionName: parsed.functionName ?? null,
|
|
144
|
+
requestId: parsed.requestId ?? null,
|
|
145
|
+
entityType: parsed.entityType ?? null,
|
|
146
|
+
entityId: parsed.entityId ?? null,
|
|
147
|
+
jobId: parsed.jobId ?? null,
|
|
148
|
+
details: sanitizeDetails(parsed.details),
|
|
149
|
+
createdAt: now.toISOString()
|
|
150
|
+
});
|
|
151
|
+
getDatabase().prepare(`INSERT INTO diagnostic_logs (
|
|
152
|
+
id, level, source, scope, event_key, message, route, function_name,
|
|
153
|
+
request_id, entity_type, entity_id, job_id, details_json, created_at
|
|
154
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.level, entry.source, entry.scope, entry.eventKey, entry.message, entry.route, entry.functionName, entry.requestId, entry.entityType, entry.entityId, entry.jobId, JSON.stringify(entry.details), entry.createdAt);
|
|
155
|
+
enforceDiagnosticLogRetention({ now });
|
|
156
|
+
return entry;
|
|
157
|
+
}
|
|
158
|
+
export function listDiagnosticLogs(filters = {}) {
|
|
159
|
+
enforceDiagnosticLogRetention();
|
|
160
|
+
const whereClauses = [];
|
|
161
|
+
const params = [];
|
|
162
|
+
if (filters.level) {
|
|
163
|
+
whereClauses.push("level = ?");
|
|
164
|
+
params.push(filters.level);
|
|
165
|
+
}
|
|
166
|
+
if (filters.source) {
|
|
167
|
+
whereClauses.push("source = ?");
|
|
168
|
+
params.push(filters.source);
|
|
169
|
+
}
|
|
170
|
+
if (filters.scope) {
|
|
171
|
+
whereClauses.push("scope = ?");
|
|
172
|
+
params.push(filters.scope);
|
|
173
|
+
}
|
|
174
|
+
if (filters.route) {
|
|
175
|
+
whereClauses.push("route = ?");
|
|
176
|
+
params.push(filters.route);
|
|
177
|
+
}
|
|
178
|
+
if (filters.entityType) {
|
|
179
|
+
whereClauses.push("entity_type = ?");
|
|
180
|
+
params.push(filters.entityType);
|
|
181
|
+
}
|
|
182
|
+
if (filters.entityId) {
|
|
183
|
+
whereClauses.push("entity_id = ?");
|
|
184
|
+
params.push(filters.entityId);
|
|
185
|
+
}
|
|
186
|
+
if (filters.jobId) {
|
|
187
|
+
whereClauses.push("job_id = ?");
|
|
188
|
+
params.push(filters.jobId);
|
|
189
|
+
}
|
|
190
|
+
if (filters.search) {
|
|
191
|
+
whereClauses.push("(message LIKE ? OR scope LIKE ? OR event_key LIKE ? OR IFNULL(route, '') LIKE ? OR details_json LIKE ?)");
|
|
192
|
+
const term = `%${filters.search}%`;
|
|
193
|
+
params.push(term, term, term, term, term);
|
|
194
|
+
}
|
|
195
|
+
if (filters.beforeCreatedAt && filters.beforeId) {
|
|
196
|
+
whereClauses.push("(created_at < ? OR (created_at = ? AND id < ?))");
|
|
197
|
+
params.push(filters.beforeCreatedAt, filters.beforeCreatedAt, filters.beforeId);
|
|
198
|
+
}
|
|
199
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
200
|
+
const limit = filters.limit ?? 200;
|
|
201
|
+
const rows = getDatabase()
|
|
202
|
+
.prepare(`SELECT id, level, source, scope, event_key, message, route, function_name,
|
|
203
|
+
request_id, entity_type, entity_id, job_id, details_json, created_at
|
|
204
|
+
FROM diagnostic_logs
|
|
205
|
+
${whereSql}
|
|
206
|
+
ORDER BY created_at DESC, id DESC
|
|
207
|
+
LIMIT ?`)
|
|
208
|
+
.all(...params, limit);
|
|
209
|
+
const logs = rows.map(mapRow);
|
|
210
|
+
const tail = rows.at(-1) ?? null;
|
|
211
|
+
return {
|
|
212
|
+
logs,
|
|
213
|
+
nextCursor: rows.length >= limit && tail
|
|
214
|
+
? {
|
|
215
|
+
beforeCreatedAt: tail.created_at,
|
|
216
|
+
beforeId: tail.id
|
|
217
|
+
}
|
|
218
|
+
: null
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function normalizeDiagnosticSource(value) {
|
|
222
|
+
return value === "ui" ||
|
|
223
|
+
value === "openclaw" ||
|
|
224
|
+
value === "agent" ||
|
|
225
|
+
value === "system" ||
|
|
226
|
+
value === "server"
|
|
227
|
+
? value
|
|
228
|
+
: "server";
|
|
229
|
+
}
|
|
230
|
+
export function serializeDiagnosticError(error) {
|
|
231
|
+
return sanitizeDiagnosticValue(error);
|
|
232
|
+
}
|
|
233
|
+
export function createDiagnosticMessage(input) {
|
|
234
|
+
const method = input.method?.toUpperCase() || "CALL";
|
|
235
|
+
const route = input.route || "unknown-route";
|
|
236
|
+
if (typeof input.statusCode === "number") {
|
|
237
|
+
return `${method} ${route} -> ${input.statusCode}`;
|
|
238
|
+
}
|
|
239
|
+
return `${method} ${route}`;
|
|
240
|
+
}
|
|
241
|
+
export function createDiagnosticTimestamp() {
|
|
242
|
+
return nowIso();
|
|
243
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getDatabase } from "../db.js";
|
|
2
|
+
import { listUsersByIds, resolveUserForMutation } from "./users.js";
|
|
3
|
+
export function setEntityOwner(entityType, entityId, userId, fallbackLabel) {
|
|
4
|
+
const user = resolveUserForMutation(userId, fallbackLabel);
|
|
5
|
+
const now = new Date().toISOString();
|
|
6
|
+
getDatabase()
|
|
7
|
+
.prepare(`INSERT INTO entity_owners (entity_type, entity_id, user_id, role, created_at, updated_at)
|
|
8
|
+
VALUES (?, ?, ?, 'owner', ?, ?)
|
|
9
|
+
ON CONFLICT(entity_type, entity_id)
|
|
10
|
+
DO UPDATE SET user_id = excluded.user_id, updated_at = excluded.updated_at`)
|
|
11
|
+
.run(entityType, entityId, user.id, now, now);
|
|
12
|
+
return { userId: user.id, user };
|
|
13
|
+
}
|
|
14
|
+
export function clearEntityOwner(entityType, entityId) {
|
|
15
|
+
getDatabase()
|
|
16
|
+
.prepare(`DELETE FROM entity_owners WHERE entity_type = ? AND entity_id = ?`)
|
|
17
|
+
.run(entityType, entityId);
|
|
18
|
+
}
|
|
19
|
+
export function getEntityOwnerId(entityType, entityId) {
|
|
20
|
+
const row = getDatabase()
|
|
21
|
+
.prepare(`SELECT user_id
|
|
22
|
+
FROM entity_owners
|
|
23
|
+
WHERE entity_type = ? AND entity_id = ?`)
|
|
24
|
+
.get(entityType, entityId);
|
|
25
|
+
return row?.user_id ?? null;
|
|
26
|
+
}
|
|
27
|
+
export function getEntityOwner(entityType, entityId) {
|
|
28
|
+
const userId = getEntityOwnerId(entityType, entityId);
|
|
29
|
+
return userId ? (listUsersByIds([userId])[0] ?? null) : null;
|
|
30
|
+
}
|
|
31
|
+
export function inferFirstOwnedUserId(candidates) {
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
if (!candidate.entityId) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const userId = getEntityOwnerId(candidate.entityType, candidate.entityId);
|
|
37
|
+
if (userId) {
|
|
38
|
+
return userId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function listOwnerRows(entityType, entityIds) {
|
|
44
|
+
if (entityIds.length === 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const placeholders = entityIds.map(() => "?").join(", ");
|
|
48
|
+
return getDatabase()
|
|
49
|
+
.prepare(`SELECT entity_id, user_id
|
|
50
|
+
FROM entity_owners
|
|
51
|
+
WHERE entity_type = ?
|
|
52
|
+
AND entity_id IN (${placeholders})`)
|
|
53
|
+
.all(entityType, ...entityIds);
|
|
54
|
+
}
|
|
55
|
+
export function buildEntityOwnerIndex(entityType, entityIds) {
|
|
56
|
+
const rows = listOwnerRows(entityType, entityIds);
|
|
57
|
+
const userIds = Array.from(new Set(rows.map((row) => row.user_id)));
|
|
58
|
+
const usersById = new Map(listUsersByIds(userIds).map((user) => [user.id, user]));
|
|
59
|
+
const index = new Map();
|
|
60
|
+
for (const entityId of entityIds) {
|
|
61
|
+
index.set(entityId, { userId: null, user: null });
|
|
62
|
+
}
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
index.set(row.entity_id, {
|
|
65
|
+
userId: row.user_id,
|
|
66
|
+
user: usersById.get(row.user_id) ?? null
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return index;
|
|
70
|
+
}
|
|
71
|
+
export function decorateOwnedEntities(entityType, entities) {
|
|
72
|
+
const ownerIndex = buildEntityOwnerIndex(entityType, entities.map((entity) => entity.id));
|
|
73
|
+
return entities.map((entity) => {
|
|
74
|
+
const owner = ownerIndex.get(entity.id) ?? { userId: null, user: null };
|
|
75
|
+
return {
|
|
76
|
+
...entity,
|
|
77
|
+
userId: owner.userId,
|
|
78
|
+
user: owner.user
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function decorateOwnedEntity(entityType, entity) {
|
|
83
|
+
return decorateOwnedEntities(entityType, [entity])[0];
|
|
84
|
+
}
|
|
85
|
+
export function filterOwnedEntities(entityType, entities, userIds) {
|
|
86
|
+
const decorated = decorateOwnedEntities(entityType, entities);
|
|
87
|
+
if (!userIds || userIds.length === 0) {
|
|
88
|
+
return decorated;
|
|
89
|
+
}
|
|
90
|
+
const allowed = new Set(userIds);
|
|
91
|
+
return decorated.filter((entity) => entity.userId !== null && allowed.has(entity.userId));
|
|
92
|
+
}
|