kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { createId } from "@paralleldrive/cuid2";
2import { relations } from "drizzle-orm";
3import {
4 boolean,
5 index,
6 integer,
7 pgTable,
8 text,
9 timestamp,
10} from "drizzle-orm/pg-core";
11
12export const userTable = pgTable("user", {
13 id: text("id")
14 .$defaultFn(() => createId())
15 .primaryKey(),
16 name: text("name").notNull(),
17 email: text("email").notNull().unique(),
18 emailVerified: boolean("email_verified")
19 .$defaultFn(() => false)
20 .notNull(),
21 image: text("image"),
22 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
23 updatedAt: timestamp("updated_at", { mode: "date" })
24 .defaultNow()
25 .$onUpdate(() => /* @__PURE__ */ new Date())
26 .notNull(),
27 isAnonymous: boolean("is_anonymous").default(false),
28});
29
30export const sessionTable = pgTable(
31 "session",
32 {
33 id: text("id").primaryKey(),
34 expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
35 token: text("token").notNull().unique(),
36 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
37 updatedAt: timestamp("updated_at", { mode: "date" })
38 .$onUpdate(() => /* @__PURE__ */ new Date())
39 .notNull(),
40 ipAddress: text("ip_address"),
41 userAgent: text("user_agent"),
42 userId: text("user_id")
43 .notNull()
44 .references(() => userTable.id, { onDelete: "cascade" }),
45 activeOrganizationId: text("active_organization_id"),
46 activeTeamId: text("active_team_id"),
47 },
48 (table) => [index("session_userId_idx").on(table.userId)],
49);
50
51export const accountTable = pgTable(
52 "account",
53 {
54 id: text("id")
55 .$defaultFn(() => createId())
56 .primaryKey(),
57 accountId: text("account_id").notNull(),
58 providerId: text("provider_id").notNull(),
59 userId: text("user_id")
60 .notNull()
61 .references(() => userTable.id, { onDelete: "cascade" }),
62 accessToken: text("access_token"),
63 refreshToken: text("refresh_token"),
64 idToken: text("id_token"),
65 accessTokenExpiresAt: timestamp("access_token_expires_at", {
66 mode: "date",
67 }),
68 refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
69 mode: "date",
70 }),
71 scope: text("scope"),
72 password: text("password"),
73 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
74 updatedAt: timestamp("updated_at", { mode: "date" })
75 .$onUpdate(() => /* @__PURE__ */ new Date())
76 .notNull(),
77 },
78 (table) => [index("account_userId_idx").on(table.userId)],
79);
80
81export const verificationTable = pgTable(
82 "verification",
83 {
84 id: text("id")
85 .$defaultFn(() => createId())
86 .primaryKey(),
87 identifier: text("identifier").notNull(),
88 value: text("value").notNull(),
89 expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
90 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
91 updatedAt: timestamp("updated_at", { mode: "date" })
92 .defaultNow()
93 .$onUpdate(() => /* @__PURE__ */ new Date())
94 .notNull(),
95 },
96 (table) => [index("verification_identifier_idx").on(table.identifier)],
97);
98
99export const workspaceTable = pgTable("workspace", {
100 id: text("id")
101 .$defaultFn(() => createId())
102 .primaryKey(),
103 name: text("name").notNull(),
104 slug: text("slug").notNull().unique(),
105 logo: text("logo"),
106 metadata: text("metadata"),
107 description: text("description"),
108 createdAt: timestamp("created_at", { mode: "date" }).notNull(),
109});
110
111export const workspaceUserTable = pgTable(
112 "workspace_member",
113 {
114 id: text("id")
115 .$defaultFn(() => createId())
116 .primaryKey(),
117 workspaceId: text("workspace_id")
118 .notNull()
119 .references(() => workspaceTable.id, {
120 onDelete: "cascade",
121 }),
122 userId: text("user_id")
123 .notNull()
124 .references(() => userTable.id, {
125 onDelete: "cascade",
126 }),
127 role: text("role").default("member").notNull(),
128 joinedAt: timestamp("joined_at", { mode: "date" }).notNull(),
129 },
130 (table) => [
131 index("workspace_member_workspaceId_idx").on(table.workspaceId),
132 index("workspace_member_userId_idx").on(table.userId),
133 ],
134);
135
136export const teamTable = pgTable(
137 "team",
138 {
139 id: text("id").primaryKey(),
140 name: text("name").notNull(),
141 workspaceId: text("workspace_id")
142 .notNull()
143 .references(() => workspaceTable.id, { onDelete: "cascade" }),
144 createdAt: timestamp("created_at").notNull(),
145 updatedAt: timestamp("updated_at").$onUpdate(
146 () => /* @__PURE__ */ new Date(),
147 ),
148 },
149 (table) => [index("team_workspaceId_idx").on(table.workspaceId)],
150);
151
152export const teamMemberTable = pgTable(
153 "team_member",
154 {
155 id: text("id").primaryKey(),
156 teamId: text("team_id")
157 .notNull()
158 .references(() => teamTable.id, { onDelete: "cascade" }),
159 userId: text("user_id")
160 .notNull()
161 .references(() => userTable.id, { onDelete: "cascade" }),
162 createdAt: timestamp("created_at"),
163 },
164 (table) => [
165 index("teamMember_teamId_idx").on(table.teamId),
166 index("teamMember_userId_idx").on(table.userId),
167 ],
168);
169
170export const invitationTable = pgTable(
171 "invitation",
172 {
173 id: text("id")
174 .$defaultFn(() => createId())
175 .primaryKey(),
176 workspaceId: text("workspace_id")
177 .notNull()
178 .references(() => workspaceTable.id, { onDelete: "cascade" }),
179 email: text("email").notNull(),
180 role: text("role"),
181 teamId: text("team_id"),
182 status: text("status").default("pending").notNull(),
183 expiresAt: timestamp("expires_at").notNull(),
184 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
185 inviterId: text("inviter_id")
186 .notNull()
187 .references(() => userTable.id, { onDelete: "cascade" }),
188 },
189 (table) => [
190 index("invitation_workspaceId_idx").on(table.workspaceId),
191 index("invitation_email_idx").on(table.email),
192 ],
193);
194
195export const projectTable = pgTable("project", {
196 id: text("id")
197 .$defaultFn(() => createId())
198 .primaryKey(),
199 workspaceId: text("workspace_id")
200 .notNull()
201 .references(() => workspaceTable.id, {
202 onDelete: "cascade",
203 onUpdate: "cascade",
204 }),
205 slug: text("slug").notNull(),
206 icon: text("icon").default("Layout"),
207 name: text("name").notNull(),
208 description: text("description"),
209 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
210 isPublic: boolean("is_public").default(false),
211});
212
213export const columnTable = pgTable(
214 "column",
215 {
216 id: text("id")
217 .$defaultFn(() => createId())
218 .primaryKey(),
219 projectId: text("project_id")
220 .notNull()
221 .references(() => projectTable.id, {
222 onDelete: "cascade",
223 onUpdate: "cascade",
224 }),
225 name: text("name").notNull(),
226 slug: text("slug").notNull(),
227 position: integer("position").notNull().default(0),
228 icon: text("icon"),
229 color: text("color"),
230 isFinal: boolean("is_final").default(false).notNull(),
231 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
232 updatedAt: timestamp("updated_at", { mode: "date" })
233 .defaultNow()
234 .$onUpdate(() => new Date())
235 .notNull(),
236 },
237 (table) => [index("column_projectId_idx").on(table.projectId)],
238);
239
240export const workflowRuleTable = pgTable(
241 "workflow_rule",
242 {
243 id: text("id")
244 .$defaultFn(() => createId())
245 .primaryKey(),
246 projectId: text("project_id")
247 .notNull()
248 .references(() => projectTable.id, {
249 onDelete: "cascade",
250 onUpdate: "cascade",
251 }),
252 integrationType: text("integration_type").notNull(),
253 eventType: text("event_type").notNull(),
254 columnId: text("column_id")
255 .notNull()
256 .references(() => columnTable.id, {
257 onDelete: "cascade",
258 onUpdate: "cascade",
259 }),
260 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
261 updatedAt: timestamp("updated_at", { mode: "date" })
262 .defaultNow()
263 .$onUpdate(() => new Date())
264 .notNull(),
265 },
266 (table) => [index("workflow_rule_projectId_idx").on(table.projectId)],
267);
268
269export const taskTable = pgTable("task", {
270 id: text("id")
271 .$defaultFn(() => createId())
272 .primaryKey(),
273 projectId: text("project_id")
274 .notNull()
275 .references(() => projectTable.id, {
276 onDelete: "cascade",
277 onUpdate: "cascade",
278 }),
279 position: integer("position").default(0),
280 number: integer("number").default(1),
281 userId: text("assignee_id").references(() => userTable.id, {
282 onDelete: "cascade",
283 onUpdate: "cascade",
284 }),
285 title: text("title").notNull(),
286 description: text("description"),
287 status: text("status").notNull().default("to-do"),
288 columnId: text("column_id").references(() => columnTable.id, {
289 onDelete: "set null",
290 onUpdate: "cascade",
291 }),
292 priority: text("priority").default("low"),
293 dueDate: timestamp("due_date", { mode: "date" }),
294 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
295});
296
297export const timeEntryTable = pgTable("time_entry", {
298 id: text("id")
299 .$defaultFn(() => createId())
300 .primaryKey(),
301 taskId: text("task_id")
302 .notNull()
303 .references(() => taskTable.id, {
304 onDelete: "cascade",
305 onUpdate: "cascade",
306 }),
307 userId: text("user_id").references(() => userTable.id, {
308 onDelete: "cascade",
309 onUpdate: "cascade",
310 }),
311 description: text("description"),
312 startTime: timestamp("start_time", { mode: "date" }).notNull(),
313 endTime: timestamp("end_time", { mode: "date" }),
314 duration: integer("duration").default(0),
315 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
316});
317
318export const activityTable = pgTable("activity", {
319 id: text("id")
320 .$defaultFn(() => createId())
321 .primaryKey(),
322 taskId: text("task_id")
323 .notNull()
324 .references(() => taskTable.id, {
325 onDelete: "cascade",
326 onUpdate: "cascade",
327 }),
328 type: text("type").notNull(),
329 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
330 userId: text("user_id").references(() => userTable.id, {
331 onDelete: "cascade",
332 onUpdate: "cascade",
333 }),
334 content: text("content"),
335 externalUserName: text("external_user_name"),
336 externalUserAvatar: text("external_user_avatar"),
337 externalSource: text("external_source"),
338 externalUrl: text("external_url"),
339});
340
341export const assetTable = pgTable(
342 "asset",
343 {
344 id: text("id")
345 .$defaultFn(() => createId())
346 .primaryKey(),
347 workspaceId: text("workspace_id")
348 .notNull()
349 .references(() => workspaceTable.id, {
350 onDelete: "cascade",
351 onUpdate: "cascade",
352 }),
353 projectId: text("project_id")
354 .notNull()
355 .references(() => projectTable.id, {
356 onDelete: "cascade",
357 onUpdate: "cascade",
358 }),
359 taskId: text("task_id").references(() => taskTable.id, {
360 onDelete: "cascade",
361 onUpdate: "cascade",
362 }),
363 activityId: text("activity_id").references(() => activityTable.id, {
364 onDelete: "cascade",
365 onUpdate: "cascade",
366 }),
367 objectKey: text("object_key").notNull().unique(),
368 filename: text("filename").notNull(),
369 mimeType: text("mime_type").notNull(),
370 size: integer("size").notNull(),
371 kind: text("kind").notNull().default("image"),
372 surface: text("surface").notNull().default("description"),
373 createdBy: text("created_by").references(() => userTable.id, {
374 onDelete: "set null",
375 onUpdate: "cascade",
376 }),
377 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
378 },
379 (table) => [
380 index("asset_workspaceId_idx").on(table.workspaceId),
381 index("asset_projectId_idx").on(table.projectId),
382 index("asset_taskId_idx").on(table.taskId),
383 index("asset_activityId_idx").on(table.activityId),
384 ],
385);
386
387export const labelTable = pgTable("label", {
388 id: text("id")
389 .$defaultFn(() => createId())
390 .primaryKey(),
391 name: text("name").notNull(),
392 color: text("color").notNull(),
393 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
394 taskId: text("task_id").references(() => taskTable.id, {
395 onDelete: "cascade",
396 onUpdate: "cascade",
397 }),
398 workspaceId: text("workspace_id").references(() => workspaceTable.id, {
399 onDelete: "cascade",
400 onUpdate: "cascade",
401 }),
402});
403
404export const notificationTable = pgTable("notification", {
405 id: text("id")
406 .$defaultFn(() => createId())
407 .primaryKey(),
408 userId: text("user_id")
409 .notNull()
410 .references(() => userTable.id, {
411 onDelete: "cascade",
412 onUpdate: "cascade",
413 }),
414 title: text("title").notNull(),
415 content: text("content"),
416 type: text("type").notNull().default("info"),
417 isRead: boolean("is_read").default(false),
418 resourceId: text("resource_id"),
419 resourceType: text("resource_type"),
420 createdAt: timestamp("created_at", { mode: "date", withTimezone: true })
421 .defaultNow()
422 .notNull(),
423});
424
425export const githubIntegrationTable = pgTable("github_integration", {
426 id: text("id")
427 .$defaultFn(() => createId())
428 .primaryKey(),
429 projectId: text("project_id")
430 .notNull()
431 .references(() => projectTable.id, {
432 onDelete: "cascade",
433 onUpdate: "cascade",
434 })
435 .unique(),
436 repositoryOwner: text("repository_owner").notNull(),
437 repositoryName: text("repository_name").notNull(),
438 installationId: integer("installation_id"),
439 isActive: boolean("is_active").default(true),
440 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
441 updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(),
442});
443
444export const integrationTable = pgTable(
445 "integration",
446 {
447 id: text("id")
448 .$defaultFn(() => createId())
449 .primaryKey(),
450 projectId: text("project_id")
451 .notNull()
452 .references(() => projectTable.id, {
453 onDelete: "cascade",
454 onUpdate: "cascade",
455 }),
456 type: text("type").notNull(),
457 config: text("config").notNull(),
458 isActive: boolean("is_active").default(true),
459 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
460 updatedAt: timestamp("updated_at", { mode: "date" })
461 .defaultNow()
462 .$onUpdate(() => new Date())
463 .notNull(),
464 },
465 (table) => [
466 index("integration_projectId_idx").on(table.projectId),
467 index("integration_type_idx").on(table.type),
468 ],
469);
470
471export const externalLinkTable = pgTable(
472 "external_link",
473 {
474 id: text("id")
475 .$defaultFn(() => createId())
476 .primaryKey(),
477 taskId: text("task_id")
478 .notNull()
479 .references(() => taskTable.id, {
480 onDelete: "cascade",
481 onUpdate: "cascade",
482 }),
483 integrationId: text("integration_id")
484 .notNull()
485 .references(() => integrationTable.id, {
486 onDelete: "cascade",
487 onUpdate: "cascade",
488 }),
489 resourceType: text("resource_type").notNull(),
490 externalId: text("external_id").notNull(),
491 url: text("url").notNull(),
492 title: text("title"),
493 metadata: text("metadata"),
494 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
495 updatedAt: timestamp("updated_at", { mode: "date" })
496 .defaultNow()
497 .$onUpdate(() => new Date())
498 .notNull(),
499 },
500 (table) => [
501 index("external_link_taskId_idx").on(table.taskId),
502 index("external_link_integrationId_idx").on(table.integrationId),
503 index("external_link_externalId_idx").on(table.externalId),
504 index("external_link_resourceType_idx").on(table.resourceType),
505 ],
506);
507
508export const apikeyTable = pgTable(
509 "apikey",
510 {
511 id: text("id")
512 .$defaultFn(() => createId())
513 .primaryKey(),
514 configId: text("config_id").default("default").notNull(),
515 name: text("name"),
516 start: text("start"),
517 referenceId: text("reference_id")
518 .notNull()
519 .references(() => userTable.id, { onDelete: "cascade" }),
520 prefix: text("prefix"),
521 key: text("key").notNull(),
522 userId: text("user_id").references(() => userTable.id, {
523 onDelete: "cascade",
524 }),
525 refillInterval: integer("refill_interval"),
526 refillAmount: integer("refill_amount"),
527 lastRefillAt: timestamp("last_refill_at", { mode: "date" }),
528 enabled: boolean("enabled").default(true),
529 rateLimitEnabled: boolean("rate_limit_enabled").default(true),
530 rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000),
531 rateLimitMax: integer("rate_limit_max").default(10),
532 requestCount: integer("request_count").default(0),
533 remaining: integer("remaining"),
534 lastRequest: timestamp("last_request", { mode: "date" }),
535 expiresAt: timestamp("expires_at", { mode: "date" }),
536 createdAt: timestamp("created_at", { mode: "date" }).notNull(),
537 updatedAt: timestamp("updated_at", { mode: "date" }).notNull(),
538 permissions: text("permissions"),
539 metadata: text("metadata"),
540 },
541 (table) => [
542 index("apikey_configId_idx").on(table.configId),
543 index("apikey_key_idx").on(table.key),
544 index("apikey_referenceId_idx").on(table.referenceId),
545 index("apikey_userId_idx").on(table.userId),
546 ],
547);
548
549// Auth-schema compatible aliases in schema.ts
550export const user = userTable;
551export const session = sessionTable;
552export const account = accountTable;
553export const verification = verificationTable;
554export const workspace = workspaceTable;
555export const team = teamTable;
556export const teamMember = teamMemberTable;
557export const workspace_member = workspaceUserTable;
558export const invitation = invitationTable;
559export const apikey = apikeyTable;
560
561// Auth-schema compatible relation exports in schema.ts
562export const userRelations = relations(user, ({ many }) => ({
563 sessions: many(session),
564 accounts: many(account),
565 teamMembers: many(teamMember),
566 workspace_members: many(workspace_member),
567 invitations: many(invitation),
568}));
569
570export const sessionRelations = relations(session, ({ one }) => ({
571 user: one(user, {
572 fields: [session.userId],
573 references: [user.id],
574 }),
575}));
576
577export const accountRelations = relations(account, ({ one }) => ({
578 user: one(user, {
579 fields: [account.userId],
580 references: [user.id],
581 }),
582}));
583
584export const workspaceRelations = relations(workspace, ({ many }) => ({
585 teams: many(team),
586 workspace_members: many(workspace_member),
587 invitations: many(invitation),
588}));
589
590export const teamRelations = relations(team, ({ one, many }) => ({
591 workspace: one(workspace, {
592 fields: [team.workspaceId],
593 references: [workspace.id],
594 }),
595 teamMembers: many(teamMember),
596}));
597
598export const teamMemberRelations = relations(teamMember, ({ one }) => ({
599 team: one(team, {
600 fields: [teamMember.teamId],
601 references: [team.id],
602 }),
603 user: one(user, {
604 fields: [teamMember.userId],
605 references: [user.id],
606 }),
607}));
608
609export const workspace_memberRelations = relations(
610 workspace_member,
611 ({ one }) => ({
612 workspace: one(workspace, {
613 fields: [workspace_member.workspaceId],
614 references: [workspace.id],
615 }),
616 user: one(user, {
617 fields: [workspace_member.userId],
618 references: [user.id],
619 }),
620 }),
621);
622
623export const invitationRelations = relations(invitation, ({ one }) => ({
624 workspace: one(workspace, {
625 fields: [invitation.workspaceId],
626 references: [workspace.id],
627 }),
628 user: one(user, {
629 fields: [invitation.inviterId],
630 references: [user.id],
631 }),
632}));