import Koa from 'koa' import Router from 'koa-router' import { getRepository } from 'typeorm' const webhookRouter = new Router() /** * Label Studio Webhook Event Types */ export enum WebhookAction { ANNOTATION_CREATED = 'ANNOTATION_CREATED', ANNOTATION_UPDATED = 'ANNOTATION_UPDATED', ANNOTATION_DELETED = 'ANNOTATION_DELETED', TASK_CREATED = 'TASK_CREATED', TASK_UPDATED = 'TASK_UPDATED', TASK_DELETED = 'TASK_DELETED', PROJECT_UPDATED = 'PROJECT_UPDATED' } export interface WebhookPayload { action: WebhookAction project: { id: number title: string } task?: { id: number data: any annotations: any[] } annotation?: { id: number result: any[] completed_by: { id: number email: string } lead_time: number } } /** * Webhook handler function type * Applications can register custom handlers for any webhook action */ export type WebhookHandler = (payload: WebhookPayload, context: Koa.Context) => Promise /** * Registry for custom webhook handlers * Multiple handlers can be registered per action */ const customHandlers: Map = new Map() /** * Register a custom webhook handler * Handlers are executed in registration order * * @param action - Webhook action to handle * @param handler - Handler function * * @example * registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => { * console.log('Custom handler:', payload.annotation?.id) * // Store annotation in database * // Trigger ML training * // Send notifications * }) */ export function registerWebhookHandler(action: WebhookAction, handler: WebhookHandler): void { if (!customHandlers.has(action)) { customHandlers.set(action, []) } customHandlers.get(action)!.push(handler) } /** * Unregister a webhook handler */ export function unregisterWebhookHandler(action: WebhookAction, handler: WebhookHandler): void { const handlers = customHandlers.get(action) if (handlers) { const index = handlers.indexOf(handler) if (index > -1) { handlers.splice(index, 1) } } } /** * Clear all custom handlers for an action */ export function clearWebhookHandlers(action?: WebhookAction): void { if (action) { customHandlers.delete(action) } else { customHandlers.clear() } } /** * Execute all registered handlers for an action */ async function executeCustomHandlers(action: WebhookAction, payload: WebhookPayload, ctx: Koa.Context): Promise { const handlers = customHandlers.get(action) if (!handlers || handlers.length === 0) { return } // Execute all handlers in sequence for (const handler of handlers) { try { await handler(payload, ctx) } catch (error) { console.error(`[Webhook] Custom handler error for ${action}:`, error) // Continue executing other handlers even if one fails } } } /** * Handle annotation created event */ async function handleAnnotationCreated(payload: WebhookPayload) { console.log(`[Webhook] Annotation created:`, { projectId: payload.project.id, taskId: payload.task?.id, annotationId: payload.annotation?.id, completedBy: payload.annotation?.completed_by?.email }) // TODO: Implement business logic // 1. Store annotation in Things-Factory database // 2. Trigger downstream processes (e.g., model training) // 3. Send notifications to stakeholders // 4. Update task status in external systems } /** * Handle annotation updated event */ async function handleAnnotationUpdated(payload: WebhookPayload) { console.log(`[Webhook] Annotation updated:`, { projectId: payload.project.id, annotationId: payload.annotation?.id }) // TODO: Update annotation in database } /** * Handle annotation deleted event */ async function handleAnnotationDeleted(payload: WebhookPayload) { console.log(`[Webhook] Annotation deleted:`, { projectId: payload.project.id, annotationId: payload.annotation?.id }) // TODO: Remove annotation from database } /** * Handle task created event */ async function handleTaskCreated(payload: WebhookPayload) { console.log(`[Webhook] Task created:`, { projectId: payload.project.id, taskId: payload.task?.id }) // TODO: Store task reference } /** * Handle project updated event */ async function handleProjectUpdated(payload: WebhookPayload) { console.log(`[Webhook] Project updated:`, { projectId: payload.project.id, projectTitle: payload.project.title }) // TODO: Update project metadata } /** * Main webhook endpoint * Label Studio will POST events here */ webhookRouter.post('/label-studio/webhook', async (ctx: Koa.Context) => { try { const payload = ctx.request.body as WebhookPayload console.log(`[Webhook] Received event: ${payload.action}`) // Execute default handlers first switch (payload.action) { case WebhookAction.ANNOTATION_CREATED: await handleAnnotationCreated(payload) break case WebhookAction.ANNOTATION_UPDATED: await handleAnnotationUpdated(payload) break case WebhookAction.ANNOTATION_DELETED: await handleAnnotationDeleted(payload) break case WebhookAction.TASK_CREATED: await handleTaskCreated(payload) break case WebhookAction.TASK_UPDATED: // Optional: handle task updates break case WebhookAction.TASK_DELETED: // Optional: handle task deletions break case WebhookAction.PROJECT_UPDATED: await handleProjectUpdated(payload) break default: console.warn(`[Webhook] Unknown action: ${payload.action}`) } // Execute custom handlers await executeCustomHandlers(payload.action, payload, ctx) ctx.status = 200 ctx.body = { success: true } } catch (error) { console.error('[Webhook] Error processing webhook:', error) ctx.status = 500 ctx.body = { error: error.message } } }) /** * Webhook registration helper * Call this to register webhook with Label Studio */ webhookRouter.post('/label-studio/webhook/register', async (ctx: Koa.Context) => { try { const { projectId } = ctx.request.body as { projectId: number } // Get Things-Factory server URL const serverUrl = process.env.SERVER_URL || 'http://localhost:3000' const webhookUrl = `${serverUrl}/label-studio/webhook` // Register webhook using Label Studio API const { labelStudioApi } = await import('../utils/label-studio-api-client.js') const webhook = await labelStudioApi.createWebhook({ project: projectId, url: webhookUrl, send_payload: true, send_for_all_actions: true, headers: { 'Content-Type': 'application/json' } }) ctx.status = 200 ctx.body = { success: true, webhook } } catch (error) { console.error('[Webhook] Error registering webhook:', error) ctx.status = 500 ctx.body = { error: error.message } } }) /** * Get webhooks for a project */ webhookRouter.get('/label-studio/webhook/:projectId', async (ctx: Koa.Context) => { try { const projectId = parseInt(ctx.params.projectId) const { labelStudioApi } = await import('../utils/label-studio-api-client.js') const webhooks = await labelStudioApi.getWebhooks(projectId) ctx.status = 200 ctx.body = webhooks } catch (error) { console.error('[Webhook] Error fetching webhooks:', error) ctx.status = 500 ctx.body = { error: error.message } } }) // Register routes with Things-Factory process.on('bootstrap-module-domain-private-route', (_app: Koa, router: Router) => { router.use(webhookRouter.routes()) router.use(webhookRouter.allowedMethods()) }) export { webhookRouter }