kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
at main 299 lines 7.7 kB view raw
1import { and, eq } from "drizzle-orm"; 2import db from "../database"; 3import { integrationTable } from "../database/schema"; 4import { subscribeToEvent } from "../events"; 5import type { 6 IntegrationPlugin, 7 PluginContext, 8 TaskCommentCreatedEvent, 9 TaskCreatedEvent, 10 TaskDescriptionChangedEvent, 11 TaskPriorityChangedEvent, 12 TaskStatusChangedEvent, 13 TaskTitleChangedEvent, 14} from "./types"; 15 16const plugins = new Map<string, IntegrationPlugin>(); 17let eventSubscriptionsInitialized = false; 18 19export function registerPlugin(plugin: IntegrationPlugin): void { 20 if (plugins.has(plugin.type)) { 21 throw new Error(`Plugin ${plugin.type} already registered`); 22 } 23 plugins.set(plugin.type, plugin); 24 console.log(`✓ Registered plugin: ${plugin.name}`); 25} 26 27export function initializeEventSubscriptions(): void { 28 if (eventSubscriptionsInitialized) { 29 return; 30 } 31 32 subscribeToEvent<{ 33 taskId: string; 34 userId: string; 35 title: string; 36 description: string; 37 priority: string; 38 status: string; 39 number: number; 40 projectId: string; 41 }>("task.created", async (data) => { 42 await broadcastTaskCreated({ 43 taskId: data.taskId, 44 projectId: data.projectId, 45 userId: data.userId, 46 title: data.title, 47 description: data.description, 48 priority: data.priority, 49 status: data.status, 50 number: data.number, 51 }); 52 }); 53 54 subscribeToEvent<{ 55 taskId: string; 56 userId: string | null; 57 oldStatus: string; 58 newStatus: string; 59 title: string; 60 projectId: string; 61 }>("task.status_changed", async (data) => { 62 await broadcastTaskStatusChanged({ 63 taskId: data.taskId, 64 projectId: data.projectId, 65 userId: data.userId, 66 oldStatus: data.oldStatus, 67 newStatus: data.newStatus, 68 title: data.title, 69 }); 70 }); 71 72 subscribeToEvent<{ 73 taskId: string; 74 userId: string | null; 75 oldPriority: string; 76 newPriority: string; 77 title: string; 78 projectId: string; 79 }>("task.priority_changed", async (data) => { 80 await broadcastTaskPriorityChanged({ 81 taskId: data.taskId, 82 projectId: data.projectId, 83 userId: data.userId, 84 oldPriority: data.oldPriority, 85 newPriority: data.newPriority, 86 title: data.title, 87 }); 88 }); 89 90 subscribeToEvent<{ 91 taskId: string; 92 userId: string | null; 93 oldTitle: string; 94 newTitle: string; 95 projectId: string; 96 }>("task.title_changed", async (data) => { 97 await broadcastTaskTitleChanged({ 98 taskId: data.taskId, 99 projectId: data.projectId, 100 userId: data.userId, 101 oldTitle: data.oldTitle, 102 newTitle: data.newTitle, 103 }); 104 }); 105 106 subscribeToEvent<{ 107 taskId: string; 108 userId: string | null; 109 oldDescription: string | null; 110 newDescription: string | null; 111 projectId: string; 112 }>("task.description_changed", async (data) => { 113 await broadcastTaskDescriptionChanged({ 114 taskId: data.taskId, 115 projectId: data.projectId, 116 userId: data.userId, 117 oldDescription: data.oldDescription, 118 newDescription: data.newDescription, 119 }); 120 }); 121 122 subscribeToEvent<{ 123 taskId: string; 124 userId: string; 125 comment: string; 126 projectId: string; 127 }>("task.comment_created", async (data) => { 128 await broadcastTaskCommentCreated({ 129 taskId: data.taskId, 130 projectId: data.projectId, 131 userId: data.userId, 132 comment: data.comment, 133 }); 134 }); 135 136 eventSubscriptionsInitialized = true; 137 console.log("✓ Plugin event subscriptions initialized"); 138} 139 140export function getPlugin(type: string): IntegrationPlugin | undefined { 141 return plugins.get(type); 142} 143 144export function listPlugins(): IntegrationPlugin[] { 145 return Array.from(plugins.values()); 146} 147 148async function getActiveIntegrations(projectId: string) { 149 return db.query.integrationTable.findMany({ 150 where: and( 151 eq(integrationTable.projectId, projectId), 152 eq(integrationTable.isActive, true), 153 ), 154 with: { 155 project: true, 156 }, 157 }); 158} 159 160function createContext(integration: { 161 id: string; 162 projectId: string; 163 config: string; 164}): PluginContext { 165 return { 166 integrationId: integration.id, 167 projectId: integration.projectId, 168 config: JSON.parse(integration.config) as Record<string, unknown>, 169 }; 170} 171 172export async function broadcastTaskCreated( 173 event: TaskCreatedEvent, 174): Promise<void> { 175 const integrations = await getActiveIntegrations(event.projectId); 176 177 for (const integration of integrations) { 178 const plugin = getPlugin(integration.type); 179 if (!plugin?.onTaskCreated) continue; 180 181 const context = createContext(integration); 182 183 try { 184 await plugin.onTaskCreated(event, context); 185 } catch (error) { 186 console.error(`Plugin ${plugin.type} error on task.created:`, error); 187 } 188 } 189} 190 191export async function broadcastTaskStatusChanged( 192 event: TaskStatusChangedEvent, 193): Promise<void> { 194 const integrations = await getActiveIntegrations(event.projectId); 195 196 for (const integration of integrations) { 197 const plugin = getPlugin(integration.type); 198 if (!plugin?.onTaskStatusChanged) continue; 199 200 const context = createContext(integration); 201 202 try { 203 await plugin.onTaskStatusChanged(event, context); 204 } catch (error) { 205 console.error( 206 `Plugin ${plugin.type} error on task.status_changed:`, 207 error, 208 ); 209 } 210 } 211} 212 213export async function broadcastTaskPriorityChanged( 214 event: TaskPriorityChangedEvent, 215): Promise<void> { 216 const integrations = await getActiveIntegrations(event.projectId); 217 218 for (const integration of integrations) { 219 const plugin = getPlugin(integration.type); 220 if (!plugin?.onTaskPriorityChanged) continue; 221 222 const context = createContext(integration); 223 224 try { 225 await plugin.onTaskPriorityChanged(event, context); 226 } catch (error) { 227 console.error( 228 `Plugin ${plugin.type} error on task.priority_changed:`, 229 error, 230 ); 231 } 232 } 233} 234 235export async function broadcastTaskTitleChanged( 236 event: TaskTitleChangedEvent, 237): Promise<void> { 238 const integrations = await getActiveIntegrations(event.projectId); 239 240 for (const integration of integrations) { 241 const plugin = getPlugin(integration.type); 242 if (!plugin?.onTaskTitleChanged) continue; 243 244 const context = createContext(integration); 245 246 try { 247 await plugin.onTaskTitleChanged(event, context); 248 } catch (error) { 249 console.error( 250 `Plugin ${plugin.type} error on task.title_changed:`, 251 error, 252 ); 253 } 254 } 255} 256 257export async function broadcastTaskDescriptionChanged( 258 event: TaskDescriptionChangedEvent, 259): Promise<void> { 260 const integrations = await getActiveIntegrations(event.projectId); 261 262 for (const integration of integrations) { 263 const plugin = getPlugin(integration.type); 264 if (!plugin?.onTaskDescriptionChanged) continue; 265 266 const context = createContext(integration); 267 268 try { 269 await plugin.onTaskDescriptionChanged(event, context); 270 } catch (error) { 271 console.error( 272 `Plugin ${plugin.type} error on task.description_changed:`, 273 error, 274 ); 275 } 276 } 277} 278 279export async function broadcastTaskCommentCreated( 280 event: TaskCommentCreatedEvent, 281): Promise<void> { 282 const integrations = await getActiveIntegrations(event.projectId); 283 284 for (const integration of integrations) { 285 const plugin = getPlugin(integration.type); 286 if (!plugin?.onTaskCommentCreated) continue; 287 288 const context = createContext(integration); 289 290 try { 291 await plugin.onTaskCommentCreated(event, context); 292 } catch (error) { 293 console.error( 294 `Plugin ${plugin.type} error on task.comment_created:`, 295 error, 296 ); 297 } 298 } 299}