kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(migration): rename active_workspace_id to active_organization_id

+311 -106
+13
apps/api/drizzle/0006_rename_active_workspace_to_organization.sql
··· 1 + -- Rename active_workspace_id to active_organization_id in session table 2 + DO $$ 3 + BEGIN 4 + IF EXISTS ( 5 + SELECT 1 FROM information_schema.columns 6 + WHERE table_name = 'session' 7 + AND column_name = 'active_workspace_id' 8 + ) THEN 9 + ALTER TABLE "session" RENAME COLUMN "active_workspace_id" TO "active_organization_id"; 10 + END IF; 11 + END $$; 12 + 13 +
+7
apps/api/drizzle/meta/_journal.json
··· 43 43 "when": 1759234332200, 44 44 "tag": "0005_jittery_monster_badoon", 45 45 "breakpoints": true 46 + }, 47 + { 48 + "idx": 6, 49 + "version": "7", 50 + "when": 1733703124000, 51 + "tag": "0006_rename_active_workspace_to_organization", 52 + "breakpoints": true 46 53 } 47 54 ] 48 55 }
+6
apps/api/src/database/index.ts
··· 5 5 accountTableRelations, 6 6 activityTableRelations, 7 7 githubIntegrationTableRelations, 8 + invitationTableRelations, 8 9 labelTableRelations, 9 10 notificationTableRelations, 10 11 projectTableRelations, 11 12 sessionTableRelations, 12 13 taskTableRelations, 14 + teamMemberTableRelations, 15 + teamTableRelations, 13 16 timeEntryTableRelations, 14 17 userTableRelations, 15 18 verificationTableRelations, ··· 66 69 verificationTableRelations, 67 70 workspaceTableRelations, 68 71 workspaceUserTableRelations, 72 + teamTableRelations, 73 + teamMemberTableRelations, 69 74 projectTableRelations, 70 75 taskTableRelations, 71 76 timeEntryTableRelations, ··· 73 78 labelTableRelations, 74 79 notificationTableRelations, 75 80 githubIntegrationTableRelations, 81 + invitationTableRelations, 76 82 }; 77 83 78 84 const db = drizzle(pool, {
+26
apps/api/src/database/relations.ts
··· 9 9 projectTable, 10 10 sessionTable, 11 11 taskTable, 12 + teamMemberTable, 13 + teamTable, 12 14 timeEntryTable, 13 15 userTable, 14 16 verificationTable, ··· 19 21 export const userTableRelations = relations(userTable, ({ many }) => ({ 20 22 sessions: many(sessionTable), 21 23 accounts: many(accountTable), 24 + teamMembers: many(teamMemberTable), 22 25 workspaces: many(workspaceTable), 23 26 workspaceMemberships: many(workspaceUserTable), 24 27 assignedTasks: many(taskTable), ··· 50 53 export const workspaceTableRelations = relations( 51 54 workspaceTable, 52 55 ({ many }) => ({ 56 + teams: many(teamTable), 53 57 members: many(workspaceUserTable), 54 58 projects: many(projectTable), 55 59 invitations: many(invitationTable), ··· 141 145 project: one(projectTable, { 142 146 fields: [githubIntegrationTable.projectId], 143 147 references: [projectTable.id], 148 + }), 149 + }), 150 + ); 151 + 152 + export const teamTableRelations = relations(teamTable, ({ one, many }) => ({ 153 + workspace: one(workspaceTable, { 154 + fields: [teamTable.workspaceId], 155 + references: [workspaceTable.id], 156 + }), 157 + teamMembers: many(teamMemberTable), 158 + })); 159 + 160 + export const teamMemberTableRelations = relations( 161 + teamMemberTable, 162 + ({ one }) => ({ 163 + team: one(teamTable, { 164 + fields: [teamMemberTable.teamId], 165 + references: [teamTable.id], 166 + }), 167 + user: one(userTable, { 168 + fields: [teamMemberTable.userId], 169 + references: [userTable.id], 144 170 }), 145 171 }), 146 172 );
+140 -94
apps/api/src/database/schema.ts
··· 1 1 import { createId } from "@paralleldrive/cuid2"; 2 2 import { 3 3 boolean, 4 + index, 4 5 integer, 5 6 pgTable, 6 7 text, 7 8 timestamp, 8 - unique, 9 9 } from "drizzle-orm/pg-core"; 10 10 11 11 export const userTable = pgTable("user", { ··· 19 19 .notNull(), 20 20 image: text("image"), 21 21 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 22 - updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), 23 - isAnonymous: boolean("is_anonymous"), 22 + updatedAt: timestamp("updated_at", { mode: "date" }) 23 + .defaultNow() 24 + .$onUpdate(() => /* @__PURE__ */ new Date()) 25 + .notNull(), 26 + isAnonymous: boolean("is_anonymous").default(false), 24 27 }); 25 28 26 - export const sessionTable = pgTable("session", { 27 - id: text("id").primaryKey(), 28 - expiresAt: timestamp("expires_at", { mode: "date" }).notNull(), 29 - token: text("token").notNull().unique(), 30 - createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 31 - updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), 32 - ipAddress: text("ip_address"), 33 - userAgent: text("user_agent"), 34 - userId: text("user_id") 35 - .notNull() 36 - .references(() => userTable.id, { onDelete: "cascade" }), 37 - activeOrganizationId: text("active_workspace_id"), 38 - activeTeamId: text("active_team_id"), 39 - }); 29 + export const sessionTable = pgTable( 30 + "session", 31 + { 32 + id: text("id").primaryKey(), 33 + expiresAt: timestamp("expires_at", { mode: "date" }).notNull(), 34 + token: text("token").notNull().unique(), 35 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 36 + updatedAt: timestamp("updated_at", { mode: "date" }) 37 + .$onUpdate(() => /* @__PURE__ */ new Date()) 38 + .notNull(), 39 + ipAddress: text("ip_address"), 40 + userAgent: text("user_agent"), 41 + userId: text("user_id") 42 + .notNull() 43 + .references(() => userTable.id, { onDelete: "cascade" }), 44 + activeOrganizationId: text("active_organization_id"), 45 + activeTeamId: text("active_team_id"), 46 + }, 47 + (table) => [index("session_userId_idx").on(table.userId)], 48 + ); 40 49 41 - export const accountTable = pgTable("account", { 42 - id: text("id") 43 - .$defaultFn(() => createId()) 44 - .primaryKey(), 45 - accountId: text("account_id").notNull(), 46 - providerId: text("provider_id").notNull(), 47 - userId: text("user_id") 48 - .notNull() 49 - .references(() => userTable.id, { onDelete: "cascade" }), 50 - accessToken: text("access_token"), 51 - refreshToken: text("refresh_token"), 52 - idToken: text("id_token"), 53 - accessTokenExpiresAt: timestamp("access_token_expires_at", { 54 - mode: "date", 55 - }), 56 - refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { 57 - mode: "date", 58 - }), 59 - scope: text("scope"), 60 - password: text("password"), 61 - createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 62 - updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), 63 - }); 50 + export const accountTable = pgTable( 51 + "account", 52 + { 53 + id: text("id") 54 + .$defaultFn(() => createId()) 55 + .primaryKey(), 56 + accountId: text("account_id").notNull(), 57 + providerId: text("provider_id").notNull(), 58 + userId: text("user_id") 59 + .notNull() 60 + .references(() => userTable.id, { onDelete: "cascade" }), 61 + accessToken: text("access_token"), 62 + refreshToken: text("refresh_token"), 63 + idToken: text("id_token"), 64 + accessTokenExpiresAt: timestamp("access_token_expires_at", { 65 + mode: "date", 66 + }), 67 + refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { 68 + mode: "date", 69 + }), 70 + scope: text("scope"), 71 + password: text("password"), 72 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 73 + updatedAt: timestamp("updated_at", { mode: "date" }) 74 + .$onUpdate(() => /* @__PURE__ */ new Date()) 75 + .notNull(), 76 + }, 77 + (table) => [index("account_userId_idx").on(table.userId)], 78 + ); 64 79 65 - export const verificationTable = pgTable("verification", { 66 - id: text("id") 67 - .$defaultFn(() => createId()) 68 - .primaryKey(), 69 - identifier: text("identifier").notNull(), 70 - value: text("value").notNull(), 71 - expiresAt: timestamp("expires_at", { mode: "date" }).notNull(), 72 - createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 73 - updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), 74 - }); 80 + export const verificationTable = pgTable( 81 + "verification", 82 + { 83 + id: text("id") 84 + .$defaultFn(() => createId()) 85 + .primaryKey(), 86 + identifier: text("identifier").notNull(), 87 + value: text("value").notNull(), 88 + expiresAt: timestamp("expires_at", { mode: "date" }).notNull(), 89 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 90 + updatedAt: timestamp("updated_at", { mode: "date" }) 91 + .defaultNow() 92 + .$onUpdate(() => /* @__PURE__ */ new Date()) 93 + .notNull(), 94 + }, 95 + (table) => [index("verification_identifier_idx").on(table.identifier)], 96 + ); 75 97 76 98 export const workspaceTable = pgTable("workspace", { 77 99 id: text("id") 78 100 .$defaultFn(() => createId()) 79 101 .primaryKey(), 80 102 name: text("name").notNull(), 81 - slug: text("slug").unique(), 103 + slug: text("slug").notNull().unique(), 82 104 logo: text("logo"), 83 105 metadata: text("metadata"), 84 106 description: text("description"), 85 - createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 107 + createdAt: timestamp("created_at", { mode: "date" }).notNull(), 86 108 }); 87 109 88 110 export const workspaceUserTable = pgTable( ··· 91 113 id: text("id") 92 114 .$defaultFn(() => createId()) 93 115 .primaryKey(), 94 - workspaceId: text("workspace_id").references(() => workspaceTable.id, { 95 - onDelete: "cascade", 96 - }), 116 + workspaceId: text("workspace_id") 117 + .notNull() 118 + .references(() => workspaceTable.id, { 119 + onDelete: "cascade", 120 + }), 97 121 userId: text("user_id") 98 122 .notNull() 99 123 .references(() => userTable.id, { 100 124 onDelete: "cascade", 101 125 }), 102 126 role: text("role").default("member").notNull(), 103 - joinedAt: timestamp("joined_at", { mode: "date" }).defaultNow().notNull(), 127 + joinedAt: timestamp("joined_at", { mode: "date" }).notNull(), 104 128 }, 105 - (table) => ({ 106 - workspaceUserUnique: unique().on(table.workspaceId, table.userId), 107 - }), 129 + (table) => [ 130 + index("workspace_member_workspaceId_idx").on(table.workspaceId), 131 + index("workspace_member_userId_idx").on(table.userId), 132 + ], 108 133 ); 109 134 110 - export const teamTable = pgTable("team", { 111 - id: text("id").primaryKey(), 112 - name: text("name").notNull(), 113 - workspaceId: text("workspace_id") 114 - .notNull() 115 - .references(() => workspaceTable.id, { onDelete: "cascade" }), 116 - createdAt: timestamp("created_at").notNull(), 117 - updatedAt: timestamp("updated_at"), 118 - }); 135 + export const teamTable = pgTable( 136 + "team", 137 + { 138 + id: text("id").primaryKey(), 139 + name: text("name").notNull(), 140 + workspaceId: text("workspace_id") 141 + .notNull() 142 + .references(() => workspaceTable.id, { onDelete: "cascade" }), 143 + createdAt: timestamp("created_at").notNull(), 144 + updatedAt: timestamp("updated_at").$onUpdate( 145 + () => /* @__PURE__ */ new Date(), 146 + ), 147 + }, 148 + (table) => [index("team_workspaceId_idx").on(table.workspaceId)], 149 + ); 119 150 120 - export const teamMemberTable = pgTable("team_member", { 121 - id: text("id").primaryKey(), 122 - teamId: text("team_id") 123 - .notNull() 124 - .references(() => teamTable.id, { onDelete: "cascade" }), 125 - userId: text("user_id") 126 - .notNull() 127 - .references(() => userTable.id, { onDelete: "cascade" }), 128 - createdAt: timestamp("created_at"), 129 - }); 151 + export const teamMemberTable = pgTable( 152 + "team_member", 153 + { 154 + id: text("id").primaryKey(), 155 + teamId: text("team_id") 156 + .notNull() 157 + .references(() => teamTable.id, { onDelete: "cascade" }), 158 + userId: text("user_id") 159 + .notNull() 160 + .references(() => userTable.id, { onDelete: "cascade" }), 161 + createdAt: timestamp("created_at"), 162 + }, 163 + (table) => [ 164 + index("teamMember_teamId_idx").on(table.teamId), 165 + index("teamMember_userId_idx").on(table.userId), 166 + ], 167 + ); 130 168 131 - export const invitationTable = pgTable("invitation", { 132 - id: text("id") 133 - .$defaultFn(() => createId()) 134 - .primaryKey(), 135 - workspaceId: text("workspace_id") 136 - .notNull() 137 - .references(() => workspaceTable.id, { onDelete: "cascade" }), 138 - email: text("email").notNull(), 139 - role: text("role"), 140 - teamId: text("team_id"), 141 - status: text("status").default("pending").notNull(), 142 - expiresAt: timestamp("expires_at").notNull(), 143 - inviterId: text("inviter_id") 144 - .notNull() 145 - .references(() => userTable.id, { onDelete: "cascade" }), 146 - }); 169 + export const invitationTable = pgTable( 170 + "invitation", 171 + { 172 + id: text("id") 173 + .$defaultFn(() => createId()) 174 + .primaryKey(), 175 + workspaceId: text("workspace_id") 176 + .notNull() 177 + .references(() => workspaceTable.id, { onDelete: "cascade" }), 178 + email: text("email").notNull(), 179 + role: text("role"), 180 + teamId: text("team_id"), 181 + status: text("status").default("pending").notNull(), 182 + expiresAt: timestamp("expires_at").notNull(), 183 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 184 + inviterId: text("inviter_id") 185 + .notNull() 186 + .references(() => userTable.id, { onDelete: "cascade" }), 187 + }, 188 + (table) => [ 189 + index("invitation_workspaceId_idx").on(table.workspaceId), 190 + index("invitation_email_idx").on(table.email), 191 + ], 192 + ); 147 193 148 194 export const projectTable = pgTable("project", { 149 195 id: text("id")
+2
apps/api/src/index.ts
··· 17 17 import search from "./search"; 18 18 import task from "./task"; 19 19 import timeEntry from "./time-entry"; 20 + import { migrateSessionColumn } from "./utils/migrate-session-column"; 20 21 import { migrateWorkspaceUserEmail } from "./utils/migrate-workspace-user-email"; 21 22 22 23 const app = new Hono<{ ··· 109 110 (async () => { 110 111 try { 111 112 await migrateWorkspaceUserEmail(); 113 + await migrateSessionColumn(); 112 114 113 115 console.log("🔄 Migrating database..."); 114 116 await migrate(db, {
+115
apps/api/src/utils/migrate-session-column.ts
··· 1 + import { sql } from "drizzle-orm"; 2 + import db from "../database"; 3 + 4 + /** 5 + * Migration script to: 6 + * 1. Rename active_workspace_id to active_organization_id in session table 7 + * 2. Add created_at column to invitation table if it doesn't exist 8 + * This runs before Drizzle migrations to ensure the column names match the schema. 9 + */ 10 + export async function migrateSessionColumn() { 11 + console.log( 12 + "🔄 Checking session table for active_workspace_id to active_organization_id migration...", 13 + ); 14 + 15 + try { 16 + // Migrate session table column 17 + const sessionTableExists = await db.execute(sql` 18 + SELECT EXISTS ( 19 + SELECT 1 20 + FROM information_schema.tables 21 + WHERE table_name = 'session' 22 + ) AS exists; 23 + `); 24 + 25 + const sessionExists = 26 + sessionTableExists.rows[0]?.exists === true || 27 + sessionTableExists.rows[0]?.exists === "t"; 28 + if (sessionExists) { 29 + // Check if active_workspace_id column exists 30 + const hasOldColumn = await db.execute(sql` 31 + SELECT column_name 32 + FROM information_schema.columns 33 + WHERE table_name = 'session' 34 + AND column_name = 'active_workspace_id' 35 + `); 36 + 37 + // Check if active_organization_id column already exists 38 + const hasNewColumn = await db.execute(sql` 39 + SELECT column_name 40 + FROM information_schema.columns 41 + WHERE table_name = 'session' 42 + AND column_name = 'active_organization_id' 43 + `); 44 + 45 + if (hasOldColumn.rows.length > 0 && hasNewColumn.rows.length === 0) { 46 + console.log( 47 + "📝 Found active_workspace_id column, renaming to active_organization_id...", 48 + ); 49 + await db.execute(sql` 50 + ALTER TABLE "session" 51 + RENAME COLUMN "active_workspace_id" TO "active_organization_id"; 52 + `); 53 + console.log( 54 + "✅ Successfully renamed active_workspace_id to active_organization_id", 55 + ); 56 + } else if (hasNewColumn.rows.length > 0) { 57 + console.log( 58 + "✅ active_organization_id column already exists — skipping migration.", 59 + ); 60 + } else if (hasOldColumn.rows.length === 0) { 61 + console.log( 62 + "🛈 active_workspace_id column does not exist — skipping migration.", 63 + ); 64 + } 65 + } else { 66 + console.log("🛈 session table does not exist — skipping migration."); 67 + } 68 + 69 + // Migrate invitation table - add created_at column 70 + console.log( 71 + "🔄 Checking invitation table for created_at column migration...", 72 + ); 73 + 74 + const invitationTableExists = await db.execute(sql` 75 + SELECT EXISTS ( 76 + SELECT 1 77 + FROM information_schema.tables 78 + WHERE table_name = 'invitation' 79 + ) AS exists; 80 + `); 81 + 82 + const invitationExists = 83 + invitationTableExists.rows[0]?.exists === true || 84 + invitationTableExists.rows[0]?.exists === "t"; 85 + 86 + if (invitationExists) { 87 + const hasCreatedAt = await db.execute(sql` 88 + SELECT column_name 89 + FROM information_schema.columns 90 + WHERE table_name = 'invitation' 91 + AND column_name = 'created_at' 92 + `); 93 + 94 + if (hasCreatedAt.rows.length === 0) { 95 + console.log("📝 Adding created_at column to invitation table..."); 96 + await db.execute(sql` 97 + ALTER TABLE "invitation" 98 + ADD COLUMN "created_at" timestamp DEFAULT NOW() NOT NULL; 99 + `); 100 + console.log( 101 + "✅ Successfully added created_at column to invitation table", 102 + ); 103 + } else { 104 + console.log( 105 + "✅ created_at column already exists in invitation table — skipping migration.", 106 + ); 107 + } 108 + } else { 109 + console.log("🛈 invitation table does not exist — skipping migration."); 110 + } 111 + } catch (error) { 112 + console.error("❌ Error during migration:", error); 113 + throw error; 114 + } 115 + }
+2 -12
apps/docs/source.config.ts
··· 1 - import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; 2 1 import { defineConfig, defineDocs } from "fumadocs-mdx/config"; 3 - import lastModified from "fumadocs-mdx/plugins/last-modified"; 4 - import { transformerTwoslash } from "fumadocs-twoslash"; 5 2 6 3 export const docs = defineDocs({ 7 4 dir: "content/docs", 8 - docs: { 9 - // postprocess: { 10 - // extractLinkReferences: true, 11 - // }, 12 - }, 5 + docs: {}, 13 6 }); 14 7 15 8 export default defineConfig({ 16 - // plugins: [lastModified()], 17 9 mdxOptions: { 18 10 rehypeCodeOptions: { 19 11 themes: { 20 12 light: "github-light", 21 13 dark: "github-dark", 22 14 }, 23 - transformers: [ 24 - // ...(rehypeCodeDefaultOptions.transformers ?? []), 25 - ], 15 + transformers: [], 26 16 }, 27 17 }, 28 18 });