kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
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}