mirror of
https://github.com/hanzoai/annotations.git
synced 2026-10-30 18:48:18 +00:00
155 lines
6.1 KiB
Go
155 lines
6.1 KiB
Go
package server
|
|
|
|
import (
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/hook"
|
|
)
|
|
|
|
// The three Base collections backing the Annotations service — one per Prisma
|
|
// model the migrated tRPC routers read/wrote. All rows carry `project` (the
|
|
// console projectId, the tenant key); the service scopes every query to the
|
|
// project in the verified capability's request. Backed by Base's encrypted
|
|
// SQLite (vault plugin KEK per org) when vault is registered; plain SQLite
|
|
// otherwise.
|
|
const (
|
|
ColQueues = "annotation_queues"
|
|
ColQueueItems = "annotation_queue_items"
|
|
ColAssignments = "annotation_queue_assignments"
|
|
)
|
|
|
|
// Queue fields — mirror the annotation_queues Prisma model. `id`, `created`,
|
|
// `updated` are Base system fields; the rest mirror the source columns.
|
|
const (
|
|
qProject = "project"
|
|
qName = "name"
|
|
qDescription = "description"
|
|
qScoreConfigIds = "scoreConfigIds" // JSON array of strings
|
|
)
|
|
|
|
// QueueItem fields — mirror annotation_queue_items. Status/ObjectType are the
|
|
// Prisma string enums; lockedAt/completedAt are nullable dates.
|
|
const (
|
|
iProject = "project"
|
|
iQueueId = "queueId"
|
|
iObjectId = "objectId"
|
|
iObjectType = "objectType" // TRACE | SESSION | OBSERVATION
|
|
iStatus = "status" // PENDING | COMPLETED
|
|
iCompletedAt = "completedAt" // nullable
|
|
iLockedAt = "lockedAt" // nullable
|
|
iLockedByUserId = "lockedByUserId" // nullable
|
|
iAnnotatorUserId = "annotatorUserId" // nullable
|
|
)
|
|
|
|
// Assignment fields — mirror annotation_queue_assignments. The user identity
|
|
// (name/email) is denormalized here so this service is self-contained: the
|
|
// console resolved it from the users table via a join; we store it on assign.
|
|
const (
|
|
aProject = "project"
|
|
aQueueId = "queueId"
|
|
aUserId = "userId"
|
|
aUserName = "userName"
|
|
aUserEmail = "userEmail"
|
|
)
|
|
|
|
// AnnotationQueueStatus / ObjectType enum string values (mirror the Prisma
|
|
// enums the source used). Stored verbatim; no custom enum kind in the schema.
|
|
const (
|
|
StatusPending = "PENDING"
|
|
StatusCompleted = "COMPLETED"
|
|
|
|
ObjectTrace = "TRACE"
|
|
ObjectSession = "SESSION"
|
|
ObjectObservation = "OBSERVATION"
|
|
)
|
|
|
|
// RegisterCollections ensures all three collections exist. Idempotent:
|
|
// re-running finds the existing collections and no-ops. Wired via OnBootstrap
|
|
// so it runs once at startup, before the ZAP listener accepts calls.
|
|
func RegisterCollections(app core.App) {
|
|
app.OnBootstrap().Bind(&hook.Handler[*core.BootstrapEvent]{
|
|
Id: "annotationCollections",
|
|
Func: func(e *core.BootstrapEvent) error {
|
|
// Run the rest of bootstrap first so the DB is initialized.
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
return EnsureCollections(app)
|
|
},
|
|
})
|
|
}
|
|
|
|
// EnsureCollections creates the three collections if absent. Exported so callers
|
|
// that bootstrap the app themselves (tests, one-shot migrations) can provision
|
|
// them directly rather than via the OnBootstrap hook. Idempotent.
|
|
func EnsureCollections(app core.App) error {
|
|
if err := ensureQueues(app); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureQueueItems(app); err != nil {
|
|
return err
|
|
}
|
|
return ensureAssignments(app)
|
|
}
|
|
|
|
func ensureQueues(app core.App) error {
|
|
if _, err := app.FindCollectionByNameOrId(ColQueues); err == nil {
|
|
return nil
|
|
}
|
|
col := core.NewBaseCollection(ColQueues)
|
|
col.Fields.Add(&core.TextField{Name: qProject, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: qName, Required: true, Max: 1024})
|
|
col.Fields.Add(&core.TextField{Name: qDescription, Max: 65536})
|
|
col.Fields.Add(&core.JSONField{Name: qScoreConfigIds, MaxSize: 65536})
|
|
col.Fields.Add(&core.AutodateField{Name: "created", OnCreate: true})
|
|
col.Fields.Add(&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true})
|
|
|
|
// One queue name per project (source enforces uniqueness on create).
|
|
col.AddIndex("idx_aq_project_name", true, "project, name", "")
|
|
col.AddIndex("idx_aq_project", false, qProject, "")
|
|
return app.Save(col)
|
|
}
|
|
|
|
func ensureQueueItems(app core.App) error {
|
|
if _, err := app.FindCollectionByNameOrId(ColQueueItems); err == nil {
|
|
return nil
|
|
}
|
|
col := core.NewBaseCollection(ColQueueItems)
|
|
col.Fields.Add(&core.TextField{Name: iProject, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: iQueueId, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: iObjectId, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: iObjectType, Required: true, Max: 32})
|
|
col.Fields.Add(&core.TextField{Name: iStatus, Required: true, Max: 32})
|
|
col.Fields.Add(&core.DateField{Name: iCompletedAt})
|
|
col.Fields.Add(&core.DateField{Name: iLockedAt})
|
|
col.Fields.Add(&core.TextField{Name: iLockedByUserId, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: iAnnotatorUserId, Max: 255})
|
|
col.Fields.Add(&core.AutodateField{Name: "created", OnCreate: true})
|
|
col.Fields.Add(&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true})
|
|
|
|
// De-dupe the same object into the same queue (source createMany
|
|
// skipDuplicates on this tuple).
|
|
col.AddIndex("idx_aqi_dedupe", true, "project, queueId, objectId, objectType", "")
|
|
col.AddIndex("idx_aqi_queue", false, "project, queueId", "")
|
|
return app.Save(col)
|
|
}
|
|
|
|
func ensureAssignments(app core.App) error {
|
|
if _, err := app.FindCollectionByNameOrId(ColAssignments); err == nil {
|
|
return nil
|
|
}
|
|
col := core.NewBaseCollection(ColAssignments)
|
|
col.Fields.Add(&core.TextField{Name: aProject, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: aQueueId, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: aUserId, Required: true, Max: 255})
|
|
col.Fields.Add(&core.TextField{Name: aUserName, Max: 1024})
|
|
col.Fields.Add(&core.TextField{Name: aUserEmail, Max: 1024})
|
|
col.Fields.Add(&core.AutodateField{Name: "created", OnCreate: true})
|
|
col.Fields.Add(&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true})
|
|
|
|
// One assignment per (project, queue, user) — source createMany
|
|
// skipDuplicates on this tuple.
|
|
col.AddIndex("idx_aqa_dedupe", true, "project, queueId, userId", "")
|
|
col.AddIndex("idx_aqa_queue", false, "project, queueId", "")
|
|
return app.Save(col)
|
|
}
|