From 6ea02b1db118386147438bc74c8c4f5572a8597d Mon Sep 17 00:00:00 2001 From: Henry <30578846+hjball@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:10:46 +0000 Subject: [PATCH] feat: send email notifications on user mentions (#372) * feat: add notifications schema * chore: gen migration * feat: add mention email template * feat: add sendMentionEmail func * feat: add repo funcs * feat: update card router to send emails on mention * fix: update the editor suggestion to show all members * feat: skip pending members in sendMentionEmails * feat: update comments to use tiptap editor --- apps/web/src/components/Editor.tsx | 135 +- .../web/src/views/card/components/Comment.tsx | 62 +- .../views/card/components/NewCommentForm.tsx | 34 +- apps/web/src/views/card/index.tsx | 25 +- packages/api/src/routers/card.ts | 41 + packages/api/src/utils/notifications.ts | 133 + .../20260207214056_AddNotificationsTable.sql | 46 + .../meta/20260207214056_snapshot.json | 3687 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/repository/board.repo.ts | 1 + packages/db/src/repository/card.repo.ts | 2 + packages/db/src/repository/member.repo.ts | 30 + .../db/src/repository/notification.repo.ts | 98 + packages/db/src/schema/index.ts | 1 + packages/db/src/schema/notifications.ts | 98 + packages/email/src/sendEmail.tsx | 6 +- packages/email/src/templates/mention.tsx | 108 + packages/shared/src/utils/index.ts | 1 + packages/shared/src/utils/mentions.ts | 22 + 19 files changed, 4442 insertions(+), 95 deletions(-) create mode 100644 packages/api/src/utils/notifications.ts create mode 100644 packages/db/migrations/20260207214056_AddNotificationsTable.sql create mode 100644 packages/db/migrations/meta/20260207214056_snapshot.json create mode 100644 packages/db/src/repository/notification.repo.ts create mode 100644 packages/db/src/schema/notifications.ts create mode 100644 packages/email/src/templates/mention.tsx create mode 100644 packages/shared/src/utils/mentions.ts diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index 3ac29061..7d82e92a 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -387,46 +387,54 @@ export interface SlashNodeAttrs { label?: string | null; } -const CommandItems: SlashCommandItem[] = [ - { - title: "Heading 1", - icon: , - command: ({ editor }) => - editor.chain().focus().setHeading({ level: 1 }).run(), - }, - { - title: "Heading 2", - icon: , - command: ({ editor }) => - editor.chain().focus().setHeading({ level: 2 }).run(), - }, - { - title: "Heading 3", - icon: , - command: ({ editor }) => - editor.chain().focus().setHeading({ level: 3 }).run(), - }, - { - title: "Bullet List", - icon: , - command: ({ editor }) => editor.chain().focus().toggleBulletList().run(), - }, - { - title: "Ordered List", - icon: , - command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(), - }, - { - title: "Blockquote", - icon: , - command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(), - }, - { - title: "Code Block", - icon: , - command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(), - }, -]; +const getCommandItems = (disableHeadings: boolean): SlashCommandItem[] => { + const headingCommands: SlashCommandItem[] = disableHeadings + ? [] + : [ + { + title: "Heading 1", + icon: , + command: ({ editor }) => + editor.chain().focus().setHeading({ level: 1 }).run(), + }, + { + title: "Heading 2", + icon: , + command: ({ editor }) => + editor.chain().focus().setHeading({ level: 2 }).run(), + }, + { + title: "Heading 3", + icon: , + command: ({ editor }) => + editor.chain().focus().setHeading({ level: 3 }).run(), + }, + ]; + + return [ + ...headingCommands, + { + title: "Bullet List", + icon: , + command: ({ editor }) => editor.chain().focus().toggleBulletList().run(), + }, + { + title: "Ordered List", + icon: , + command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(), + }, + { + title: "Blockquote", + icon: , + command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(), + }, + { + title: "Code Block", + icon: , + command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(), + }, + ]; +}; export default function Editor({ content, @@ -435,6 +443,8 @@ export default function Editor({ readOnly = false, workspaceMembers, enableYouTubeEmbed = true, + placeholder, + disableHeadings = false, }: { content: string | null; onChange?: (value: string) => void; @@ -442,18 +452,23 @@ export default function Editor({ readOnly?: boolean; workspaceMembers: WorkspaceMember[]; enableYouTubeEmbed?: boolean; + placeholder?: string; + disableHeadings?: boolean; }) { const containerRef = useRef(null); const editor = useEditor( { extensions: [ - StarterKit, + StarterKit.configure({ + heading: disableHeadings ? false : undefined, + }), Markdown, Placeholder.configure({ placeholder: readOnly ? "" - : t`Add description... (type '/' to open commands or '@' to mention)`, + : placeholder ?? + t`Add description... (type '/' to open commands or '@' to mention)`, }), Link.configure({ openOnClick: true, @@ -466,10 +481,10 @@ export default function Editor({ autolink: true, }), SlashCommands.configure({ - commandItems: CommandItems, + commandItems: getCommandItems(disableHeadings), suggestion: { items: ({ query }: { query: string }) => - filterSlashCommandItems(CommandItems, query), + filterSlashCommandItems(getCommandItems(disableHeadings), query), startOfLine: true, char: "/", }, @@ -481,20 +496,28 @@ export default function Editor({ suggestion: { char: "@", items: ({ query }: { query: string }) => { - const all: MentionItem[] = workspaceMembers.map( - (member: WorkspaceMember) => ({ - id: member.publicId, - label: member?.user?.name ?? member.email, - image: member?.user?.image ?? null, - }), + const withEmail = workspaceMembers.filter((member) => member.email); + + const mapped = withEmail.map((member: WorkspaceMember) => ({ + id: member.publicId, + label: member?.user?.name?.trim() || member.email || "", + image: member?.user?.image ?? null, + })); + + const all: MentionItem[] = mapped.filter( + (item) => item.label && item.label.length > 0, ); - const q = query.toLowerCase(); - return all.filter( - (u) => - u.label && - typeof u.label === "string" && - u.label.toLowerCase().includes(q), + + const q = query.toLowerCase().trim(); + + if (q === "") { + return all; + } + + const filtered = all.filter((u) => + u.label.toLowerCase().includes(q), ); + return filtered; }, command: ({ editor, range, props }) => { const id = props.id ?? ""; diff --git a/apps/web/src/views/card/components/Comment.tsx b/apps/web/src/views/card/components/Comment.tsx index e9587c36..6653c563 100644 --- a/apps/web/src/views/card/components/Comment.tsx +++ b/apps/web/src/views/card/components/Comment.tsx @@ -1,12 +1,13 @@ import { t } from "@lingui/core/macro"; import { formatDistanceToNow } from "date-fns"; import { useState } from "react"; -import ContentEditable from "react-contenteditable"; import { useForm } from "react-hook-form"; import { HiEllipsisHorizontal, HiPencil, HiTrash } from "react-icons/hi2"; import Avatar from "~/components/Avatar"; import Button from "~/components/Button"; +import Editor from "~/components/Editor"; +import type { WorkspaceMember } from "~/components/Editor"; import Dropdown from "~/components/Dropdown"; import { usePermissions } from "~/hooks/usePermissions"; import { useModal } from "~/providers/modal"; @@ -29,7 +30,6 @@ const Comment = ({ createdAt, comment, isAuthor, - isAdmin, isEdited = false, isViewOnly = false, }: { @@ -42,7 +42,6 @@ const Comment = ({ createdAt: string; comment: string | undefined; isAuthor: boolean; - isAdmin: boolean; isEdited: boolean; isViewOnly: boolean; }) => { @@ -57,6 +56,30 @@ const Comment = ({ }, }); + const { data: cardData } = api.card.byId.useQuery( + { + cardPublicId, + }, + { + enabled: !!cardPublicId && cardPublicId.length >= 12, + }, + ); + + const workspaceMembers: WorkspaceMember[] = + cardData?.list.board.workspace.members + .filter((member) => member.email) + .map((member) => ({ + publicId: member.publicId, + email: member.email, + user: member.user + ? { + id: member.user.id, + name: member.user.name ?? null, + image: member.user.image ?? null, + } + : null, + })) ?? []; + if (!publicId) return null; const updateCommentMutation = api.card.updateComment.useMutation({ @@ -142,21 +165,28 @@ const Comment = ({ )} {!isEditing ? ( - +
+ +
) : (
- setValue("comment", e.target.value)} - className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-sm text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6" - /> -
+
+ setValue("comment", value)} + workspaceMembers={workspaceMembers} + enableYouTubeEmbed={false} + placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`} + disableHeadings={true} + /> +
+
@@ -431,7 +447,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
{!isTemplate && (
- +
)} diff --git a/packages/api/src/routers/card.ts b/packages/api/src/routers/card.ts index b39256c5..949804d1 100644 --- a/packages/api/src/routers/card.ts +++ b/packages/api/src/routers/card.ts @@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { mergeActivities } from "../utils/activities"; +import { sendMentionEmails } from "../utils/notifications"; import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions"; import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils"; @@ -153,6 +154,17 @@ export const cardRouter = createTRPCRouter({ await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert); } + if (input.description) { + sendMentionEmails({ + db: ctx.db, + cardPublicId: newCard.publicId, + commentHtml: input.description, + commenterUserId: userId, + }).catch((error) => { + console.error("Failed to send mention emails:", error); + }); + } + return newCard; }), addComment: protectedProcedure @@ -215,6 +227,16 @@ export const cardRouter = createTRPCRouter({ createdBy: userId, }); + sendMentionEmails({ + db: ctx.db, + cardPublicId: input.cardPublicId, + commentHtml: input.comment, + commenterUserId: userId, + commentId: newComment.id, + }).catch((error) => { + console.error("Failed to send mention emails:", error); + }); + return newComment; }), updateComment: protectedProcedure @@ -295,6 +317,16 @@ export const cardRouter = createTRPCRouter({ createdBy: userId, }); + sendMentionEmails({ + db: ctx.db, + cardPublicId: input.cardPublicId, + commentHtml: input.comment, + commenterUserId: userId, + commentId: updatedComment.id, + }).catch((error) => { + console.error("Failed to send mention emails:", error); + }); + return updatedComment; }), deleteComment: protectedProcedure @@ -924,6 +956,15 @@ export const cardRouter = createTRPCRouter({ fromDescription: existingCard.description ?? undefined, toDescription: input.description, }); + + sendMentionEmails({ + db: ctx.db, + cardPublicId: input.cardPublicId, + commentHtml: input.description, + commenterUserId: userId, + }).catch((error) => { + console.error("Failed to send mention emails:", error); + }); } if ( diff --git a/packages/api/src/utils/notifications.ts b/packages/api/src/utils/notifications.ts new file mode 100644 index 00000000..32b32cc9 --- /dev/null +++ b/packages/api/src/utils/notifications.ts @@ -0,0 +1,133 @@ +import { env } from "next-runtime-env"; + +import type { dbClient } from "@kan/db/client"; +import * as cardRepo from "@kan/db/repository/card.repo"; +import * as memberRepo from "@kan/db/repository/member.repo"; +import * as notificationRepo from "@kan/db/repository/notification.repo"; +import * as userRepo from "@kan/db/repository/user.repo"; +import * as workspaceRepo from "@kan/db/repository/workspace.repo"; +import { sendEmail } from "@kan/email"; +import { parseMentionsFromHTML } from "@kan/shared/utils"; + +/** + * Sends mention notification emails to mentioned members + * Only sends emails for new mentions (checks notification table to avoid duplicates) + */ +export async function sendMentionEmails({ + db, + cardPublicId, + commentHtml, + commenterUserId, + commentId, +}: { + db: dbClient; + cardPublicId: string; + commentHtml: string; + commenterUserId: string; + commentId?: number; +}) { + try { + // Parse mentions from HTML + const mentionPublicIds = parseMentionsFromHTML(commentHtml); + if (mentionPublicIds.length === 0) return; + + // Get card with board information + const card = await cardRepo.getWithListAndMembersByPublicId(db, cardPublicId); + if (!card?.list.board) return; + + const board = card.list.board; + const boardName = board.name; + const cardTitle = card.title; + const cardId = card.id; + + // Get workspace ID from workspace publicId + const workspace = await workspaceRepo.getByPublicId( + db, + board.workspace.publicId, + ); + if (!workspace?.id) return; + + const workspaceId = workspace.id; + + // Get commenter information + const commenter = await userRepo.getById(db, commenterUserId); + if (!commenter) return; + + const commenterName = commenter.name ?? commenter.email; + + // Get mentioned members with full details (filtered by workspace) + const membersWithDetails = await memberRepo.getByPublicIdsWithUsers( + db, + mentionPublicIds, + workspaceId, + ); + + // Filter out the commenter + const membersToNotify = membersWithDetails.filter( + (member) => member.user?.id !== commenterUserId, + ); + + if (membersToNotify.length === 0) return; + + const baseUrl = env("NEXT_PUBLIC_BASE_URL"); + const cardUrl = `${baseUrl}/cards/${cardPublicId}`; + + // Send emails to all mentioned members (only if notification doesn't exist) + await Promise.all( + membersToNotify.map(async (member) => { + const userId = member.user?.id; + const email = member.user?.email ?? member.email; + + // Skip pending members (no userId) - they can be mentioned but won't receive emails + if (!userId || !email) return; + + try { + // Check if notification already exists for this mention + const notificationExists = await notificationRepo.exists(db, { + userId, + cardId, + type: "mention", + }); + + // If notification already exists, skip sending email + if (notificationExists) { + return; + } + + // Create notification record + await notificationRepo.create(db, { + type: "mention", + userId, + cardId, + commentId, + }); + + // Send email + await sendEmail( + email, + `${commenterName} mentioned you in a comment on ${cardTitle}`, + "MENTION", + { + commenterName, + boardName, + cardTitle, + cardUrl, + }, + ); + } catch (error) { + console.error("Failed to send mention email:", { + email, + cardPublicId, + error: error instanceof Error ? error.message : String(error), + }); + } + }), + ); + } catch (error) { + console.error("Error sending mention emails:", { + cardPublicId, + error: error instanceof Error ? error.message : String(error), + }); + } +} + diff --git a/packages/db/migrations/20260207214056_AddNotificationsTable.sql b/packages/db/migrations/20260207214056_AddNotificationsTable.sql new file mode 100644 index 00000000..f5c792cd --- /dev/null +++ b/packages/db/migrations/20260207214056_AddNotificationsTable.sql @@ -0,0 +1,46 @@ +CREATE TYPE "public"."notification_type" AS ENUM('mention', 'workspace.member.added', 'workspace.member.removed', 'workspace.role.changed');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "notification" ( + "id" bigserial PRIMARY KEY NOT NULL, + "publicId" varchar(12) NOT NULL, + "type" "notification_type" NOT NULL, + "userId" uuid NOT NULL, + "cardId" bigint, + "commentId" bigint, + "workspaceId" bigint, + "metadata" text, + "readAt" timestamp, + "createdAt" timestamp DEFAULT now() NOT NULL, + "deletedAt" timestamp, + CONSTRAINT "notification_publicId_unique" UNIQUE("publicId") +); +--> statement-breakpoint +ALTER TABLE "notification" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notification" ADD CONSTRAINT "notification_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "public"."card"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notification" ADD CONSTRAINT "notification_commentId_card_comments_id_fk" FOREIGN KEY ("commentId") REFERENCES "public"."card_comments"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notification" ADD CONSTRAINT "notification_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_deleted_idx" ON "notification" USING btree ("userId","deletedAt");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_read_deleted_idx" ON "notification" USING btree ("userId","readAt","deletedAt");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_type_card_idx" ON "notification" USING btree ("userId","type","cardId");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_type_workspace_idx" ON "notification" USING btree ("userId","type","workspaceId");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_created_idx" ON "notification" USING btree ("userId","createdAt"); \ No newline at end of file diff --git a/packages/db/migrations/meta/20260207214056_snapshot.json b/packages/db/migrations/meta/20260207214056_snapshot.json new file mode 100644 index 00000000..efc779da --- /dev/null +++ b/packages/db/migrations/meta/20260207214056_snapshot.json @@ -0,0 +1,3687 @@ +{ + "id": "99b49af4-cc01-43ef-9d34-de8a49a61573", + "prevId": "3d6e154d-19ee-4e43-8b74-954a644f70d2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.apiKey": { + "name": "apiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "board_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "type": { + "name": "type", + "type": "board_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'regular'" + }, + "sourceBoardId": { + "name": "sourceBoardId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "board_visibility_idx": { + "name": "board_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_type_idx": { + "name": "board_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_source_idx": { + "name": "board_source_idx", + "columns": [ + { + "expression": "sourceBoardId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_slug_per_workspace": { + "name": "unique_slug_per_workspace", + "columns": [ + { + "expression": "workspaceId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"board\".\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_createdBy_user_id_fk": { + "name": "board_createdBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "board_deletedBy_user_id_fk": { + "name": "board_deletedBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "board_importId_import_id_fk": { + "name": "board_importId_import_id_fk", + "tableFrom": "board", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_workspaceId_workspace_id_fk": { + "name": "board_workspaceId_workspace_id_fk", + "tableFrom": "board", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user_board_favorites": { + "name": "user_board_favorites", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_board_favorite_user_idx": { + "name": "user_board_favorite_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_board_favorite_board_idx": { + "name": "user_board_favorite_board_idx", + "columns": [ + { + "expression": "boardId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_board_favorites_userId_user_id_fk": { + "name": "user_board_favorites_userId_user_id_fk", + "tableFrom": "user_board_favorites", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_board_favorites_boardId_board_id_fk": { + "name": "user_board_favorites_boardId_board_id_fk", + "tableFrom": "user_board_favorites", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_board_favorites_userId_boardId_pk": { + "name": "user_board_favorites_userId_boardId_pk", + "columns": [ + "userId", + "boardId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.card_activity": { + "name": "card_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "card_activity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "fromIndex": { + "name": "fromIndex", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "toIndex": { + "name": "toIndex", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fromListId": { + "name": "fromListId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "toListId": { + "name": "toListId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "fromTitle": { + "name": "fromTitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toTitle": { + "name": "toTitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fromDescription": { + "name": "fromDescription", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toDescription": { + "name": "toDescription", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "commentId": { + "name": "commentId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "fromComment": { + "name": "fromComment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toComment": { + "name": "toComment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fromDueDate": { + "name": "fromDueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "toDueDate": { + "name": "toDueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sourceBoardId": { + "name": "sourceBoardId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_activity_cardId_card_id_fk": { + "name": "card_activity_cardId_card_id_fk", + "tableFrom": "card_activity", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_fromListId_list_id_fk": { + "name": "card_activity_fromListId_list_id_fk", + "tableFrom": "card_activity", + "tableTo": "list", + "columnsFrom": [ + "fromListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_toListId_list_id_fk": { + "name": "card_activity_toListId_list_id_fk", + "tableFrom": "card_activity", + "tableTo": "list", + "columnsFrom": [ + "toListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_labelId_label_id_fk": { + "name": "card_activity_labelId_label_id_fk", + "tableFrom": "card_activity", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_workspaceMemberId_workspace_members_id_fk": { + "name": "card_activity_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "card_activity", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_activity_createdBy_user_id_fk": { + "name": "card_activity_createdBy_user_id_fk", + "tableFrom": "card_activity", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_activity_commentId_card_comments_id_fk": { + "name": "card_activity_commentId_card_comments_id_fk", + "tableFrom": "card_activity", + "tableTo": "card_comments", + "columnsFrom": [ + "commentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_activity_sourceBoardId_board_id_fk": { + "name": "card_activity_sourceBoardId_board_id_fk", + "tableFrom": "card_activity", + "tableTo": "board", + "columnsFrom": [ + "sourceBoardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_activity_publicId_unique": { + "name": "card_activity_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_attachment": { + "name": "card_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "originalFilename": { + "name": "originalFilename", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "contentType": { + "name": "contentType", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_attachment_cardId_card_id_fk": { + "name": "card_attachment_cardId_card_id_fk", + "tableFrom": "card_attachment", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_attachment_createdBy_user_id_fk": { + "name": "card_attachment_createdBy_user_id_fk", + "tableFrom": "card_attachment", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_attachment_publicId_unique": { + "name": "card_attachment_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public._card_workspace_members": { + "name": "_card_workspace_members", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_workspace_members_cardId_card_id_fk": { + "name": "_card_workspace_members_cardId_card_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_workspace_members_workspaceMemberId_workspace_members_id_fk": { + "name": "_card_workspace_members_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_workspace_members_cardId_workspaceMemberId_pk": { + "name": "_card_workspace_members_cardId_workspaceMemberId_pk", + "columns": [ + "cardId", + "workspaceMemberId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card": { + "name": "card", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_createdBy_user_id_fk": { + "name": "card_createdBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_deletedBy_user_id_fk": { + "name": "card_deletedBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_listId_list_id_fk": { + "name": "card_listId_list_id_fk", + "tableFrom": "card", + "tableTo": "list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_importId_import_id_fk": { + "name": "card_importId_import_id_fk", + "tableFrom": "card", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public._card_labels": { + "name": "_card_labels", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_labels_cardId_card_id_fk": { + "name": "_card_labels_cardId_card_id_fk", + "tableFrom": "_card_labels", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_labels_labelId_label_id_fk": { + "name": "_card_labels_labelId_label_id_fk", + "tableFrom": "_card_labels", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_labels_cardId_labelId_pk": { + "name": "_card_labels_cardId_labelId_pk", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_comments": { + "name": "card_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_comments_cardId_card_id_fk": { + "name": "card_comments_cardId_card_id_fk", + "tableFrom": "card_comments", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_comments_createdBy_user_id_fk": { + "name": "card_comments_createdBy_user_id_fk", + "tableFrom": "card_comments", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_comments_deletedBy_user_id_fk": { + "name": "card_comments_deletedBy_user_id_fk", + "tableFrom": "card_comments", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_comments_publicId_unique": { + "name": "card_comments_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_checklist_item": { + "name": "card_checklist_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "completed": { + "name": "completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checklistId": { + "name": "checklistId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_checklist_item_checklistId_card_checklist_id_fk": { + "name": "card_checklist_item_checklistId_card_checklist_id_fk", + "tableFrom": "card_checklist_item", + "tableTo": "card_checklist", + "columnsFrom": [ + "checklistId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_checklist_item_createdBy_user_id_fk": { + "name": "card_checklist_item_createdBy_user_id_fk", + "tableFrom": "card_checklist_item", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_checklist_item_deletedBy_user_id_fk": { + "name": "card_checklist_item_deletedBy_user_id_fk", + "tableFrom": "card_checklist_item", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_checklist_item_publicId_unique": { + "name": "card_checklist_item_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.card_checklist": { + "name": "card_checklist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_checklist_cardId_card_id_fk": { + "name": "card_checklist_cardId_card_id_fk", + "tableFrom": "card_checklist", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_checklist_createdBy_user_id_fk": { + "name": "card_checklist_createdBy_user_id_fk", + "tableFrom": "card_checklist", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "card_checklist_deletedBy_user_id_fk": { + "name": "card_checklist_deletedBy_user_id_fk", + "tableFrom": "card_checklist", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_checklist_publicId_unique": { + "name": "card_checklist_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "feedback_createdBy_user_id_fk": { + "name": "feedback_createdBy_user_id_fk", + "tableFrom": "feedback", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.import": { + "name": "import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_createdBy_user_id_fk": { + "name": "import_createdBy_user_id_fk", + "tableFrom": "import", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "import_publicId_unique": { + "name": "import_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.label": { + "name": "label", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "label_createdBy_user_id_fk": { + "name": "label_createdBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "label_boardId_board_id_fk": { + "name": "label_boardId_board_id_fk", + "tableFrom": "label", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "label_importId_import_id_fk": { + "name": "label_importId_import_id_fk", + "tableFrom": "label", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_deletedBy_user_id_fk": { + "name": "label_deletedBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.list": { + "name": "list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_createdBy_user_id_fk": { + "name": "list_createdBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "list_deletedBy_user_id_fk": { + "name": "list_deletedBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "list_boardId_board_id_fk": { + "name": "list_boardId_board_id_fk", + "tableFrom": "list", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_importId_import_id_fk": { + "name": "list_importId_import_id_fk", + "tableFrom": "list", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v4()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refreshToken": { + "name": "refreshToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_userId_user_id_fk": { + "name": "integration_userId_user_id_fk", + "tableFrom": "integration", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_pkey": { + "name": "integration_pkey", + "columns": [ + "userId", + "provider" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_slug_checks": { + "name": "workspace_slug_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reserved": { + "name": "reserved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_slug_checks_workspaceId_workspace_id_fk": { + "name": "workspace_slug_checks_workspaceId_workspace_id_fk", + "tableFrom": "workspace_slug_checks", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_slug_checks_createdBy_user_id_fk": { + "name": "workspace_slug_checks_createdBy_user_id_fk", + "tableFrom": "workspace_slug_checks", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_slugs": { + "name": "workspace_slugs", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "slug_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slugs_slug_unique": { + "name": "workspace_slugs_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_members_userId_user_id_fk": { + "name": "workspace_members_userId_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_members_workspaceId_workspace_id_fk": { + "name": "workspace_members_workspaceId_workspace_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_deletedBy_user_id_fk": { + "name": "workspace_members_deletedBy_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_members_roleId_workspace_roles_id_fk": { + "name": "workspace_members_roleId_workspace_roles_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_publicId_unique": { + "name": "workspace_members_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "workspace_plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "showEmailsToMembers": { + "name": "showEmailsToMembers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_createdBy_user_id_fk": { + "name": "workspace_createdBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_deletedBy_user_id_fk": { + "name": "workspace_deletedBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_publicId_unique": { + "name": "workspace_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "referenceId": { + "name": "referenceId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "periodStart": { + "name": "periodStart", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "periodEnd": { + "name": "periodEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelAtPeriodEnd": { + "name": "cancelAtPeriodEnd", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unlimitedSeats": { + "name": "unlimitedSeats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trialStart": { + "name": "trialStart", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trialEnd": { + "name": "trialEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "subscription_referenceId_workspace_publicId_fk": { + "name": "subscription_referenceId_workspace_publicId_fk", + "tableFrom": "subscription", + "tableTo": "workspace", + "columnsFrom": [ + "referenceId" + ], + "columnsTo": [ + "publicId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_invite_links": { + "name": "workspace_invite_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invite_link_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updatedBy": { + "name": "updatedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invite_links_workspaceId_workspace_id_fk": { + "name": "workspace_invite_links_workspaceId_workspace_id_fk", + "tableFrom": "workspace_invite_links", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_links_createdBy_user_id_fk": { + "name": "workspace_invite_links_createdBy_user_id_fk", + "tableFrom": "workspace_invite_links", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_invite_links_updatedBy_user_id_fk": { + "name": "workspace_invite_links_updatedBy_user_id_fk", + "tableFrom": "workspace_invite_links", + "tableTo": "user", + "columnsFrom": [ + "updatedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invite_links_publicId_unique": { + "name": "workspace_invite_links_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_invite_links_code_unique": { + "name": "workspace_invite_links_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_member_permissions": { + "name": "workspace_member_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "granted": { + "name": "granted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_member_permission": { + "name": "unique_member_permission", + "columns": [ + { + "expression": "workspaceMemberId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_member_idx": { + "name": "permission_member_idx", + "columns": [ + { + "expression": "workspaceMemberId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_role_permissions": { + "name": "workspace_role_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspaceRoleId": { + "name": "workspaceRoleId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "granted": { + "name": "granted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_role_permission": { + "name": "unique_role_permission", + "columns": [ + { + "expression": "workspaceRoleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_permissions_role_idx": { + "name": "role_permissions_role_idx", + "columns": [ + { + "expression": "workspaceRoleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_role_permissions_workspaceRoleId_workspace_roles_id_fk": { + "name": "workspace_role_permissions_workspaceRoleId_workspace_roles_id_fk", + "tableFrom": "workspace_role_permissions", + "tableTo": "workspace_roles", + "columnsFrom": [ + "workspaceRoleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.workspace_roles": { + "name": "workspace_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "hierarchyLevel": { + "name": "hierarchyLevel", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_role_per_workspace": { + "name": "unique_role_per_workspace", + "columns": [ + { + "expression": "workspaceId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_roles_workspace_idx": { + "name": "workspace_roles_workspace_idx", + "columns": [ + { + "expression": "workspaceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_roles_workspaceId_workspace_id_fk": { + "name": "workspace_roles_workspaceId_workspace_id_fk", + "tableFrom": "workspace_roles", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_roles_publicId_unique": { + "name": "workspace_roles_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "commentId": { + "name": "commentId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "readAt": { + "name": "readAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notification_user_deleted_idx": { + "name": "notification_user_deleted_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deletedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_read_deleted_idx": { + "name": "notification_user_read_deleted_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "readAt", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deletedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_type_card_idx": { + "name": "notification_user_type_card_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cardId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_type_workspace_idx": { + "name": "notification_user_type_workspace_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspaceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_created_idx": { + "name": "notification_user_created_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_userId_user_id_fk": { + "name": "notification_userId_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_cardId_card_id_fk": { + "name": "notification_cardId_card_id_fk", + "tableFrom": "notification", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_commentId_card_comments_id_fk": { + "name": "notification_commentId_card_comments_id_fk", + "tableFrom": "notification", + "tableTo": "card_comments", + "columnsFrom": [ + "commentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_workspaceId_workspace_id_fk": { + "name": "notification_workspaceId_workspace_id_fk", + "tableFrom": "notification", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_publicId_unique": { + "name": "notification_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": { + "public.board_type": { + "name": "board_type", + "schema": "public", + "values": [ + "regular", + "template" + ] + }, + "public.board_visibility": { + "name": "board_visibility", + "schema": "public", + "values": [ + "private", + "public" + ] + }, + "public.card_activity_type": { + "name": "card_activity_type", + "schema": "public", + "values": [ + "card.created", + "card.updated.title", + "card.updated.description", + "card.updated.index", + "card.updated.list", + "card.updated.label.added", + "card.updated.label.removed", + "card.updated.member.added", + "card.updated.member.removed", + "card.updated.comment.added", + "card.updated.comment.updated", + "card.updated.comment.deleted", + "card.updated.checklist.added", + "card.updated.checklist.renamed", + "card.updated.checklist.deleted", + "card.updated.checklist.item.added", + "card.updated.checklist.item.updated", + "card.updated.checklist.item.completed", + "card.updated.checklist.item.uncompleted", + "card.updated.checklist.item.deleted", + "card.updated.attachment.added", + "card.updated.attachment.removed", + "card.updated.dueDate.added", + "card.updated.dueDate.updated", + "card.updated.dueDate.removed", + "card.archived" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "trello" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "started", + "success", + "failed" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "member", + "guest" + ] + }, + "public.member_status": { + "name": "member_status", + "schema": "public", + "values": [ + "invited", + "active", + "removed", + "paused" + ] + }, + "public.slug_type": { + "name": "slug_type", + "schema": "public", + "values": [ + "reserved", + "premium" + ] + }, + "public.workspace_plan": { + "name": "workspace_plan", + "schema": "public", + "values": [ + "free", + "pro", + "enterprise" + ] + }, + "public.invite_link_status": { + "name": "invite_link_status", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "mention", + "workspace.member.added", + "workspace.member.removed", + "workspace.role.changed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index b514daac..85dfcf6c 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1769983198190, "tag": "20260201215958_AddUserBoardFavouritesTable", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1770500457005, + "tag": "20260207214056_AddNotificationsTable", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/repository/board.repo.ts b/packages/db/src/repository/board.repo.ts index 7878b20a..3d709f41 100644 --- a/packages/db/src/repository/board.repo.ts +++ b/packages/db/src/repository/board.repo.ts @@ -212,6 +212,7 @@ export const getByPublicId = async ( columns: { publicId: true, email: true, + status: true, }, with: { user: { diff --git a/packages/db/src/repository/card.repo.ts b/packages/db/src/repository/card.repo.ts index 805ddce0..5dd08580 100644 --- a/packages/db/src/repository/card.repo.ts +++ b/packages/db/src/repository/card.repo.ts @@ -420,6 +420,7 @@ export const getWithListAndMembersByPublicId = async ( ) => { const card = await db.query.cards.findFirst({ columns: { + id: true, publicId: true, title: true, description: true, @@ -507,6 +508,7 @@ export const getWithListAndMembersByPublicId = async ( columns: { publicId: true, email: true, + status: true, }, with: { user: { diff --git a/packages/db/src/repository/member.repo.ts b/packages/db/src/repository/member.repo.ts index 5aba6423..4035784e 100644 --- a/packages/db/src/repository/member.repo.ts +++ b/packages/db/src/repository/member.repo.ts @@ -63,6 +63,36 @@ export const getById = async (db: dbClient, memberId: number) => { }); }; +export const getByPublicIdsWithUsers = async ( + db: dbClient, + memberPublicIds: string[], + workspaceId?: number, +) => { + return db.query.workspaceMembers.findMany({ + where: (members, { inArray: inArrayFn, eq, and, isNull: isNullFn }) => { + const conditions = [inArrayFn(members.publicId, memberPublicIds)]; + + if (workspaceId) { + conditions.push(eq(members.workspaceId, workspaceId)); + } + + conditions.push(eq(members.status, "active")); + conditions.push(isNullFn(members.deletedAt)); + + return and(...conditions); + }, + with: { + user: { + columns: { + id: true, + name: true, + email: true, + }, + }, + }, + }); +}; + export const getByEmailAndStatus = async ( db: dbClient, email: string, diff --git a/packages/db/src/repository/notification.repo.ts b/packages/db/src/repository/notification.repo.ts new file mode 100644 index 00000000..67bd2b4c --- /dev/null +++ b/packages/db/src/repository/notification.repo.ts @@ -0,0 +1,98 @@ +import { and, count, eq, isNull } from "drizzle-orm"; + +import type { dbClient } from "@kan/db/client"; +import type { NotificationType } from "@kan/db/schema"; +import { notifications } from "@kan/db/schema"; +import { generateUID } from "@kan/shared/utils"; + +export const create = async ( + db: dbClient, + notificationInput: { + type: NotificationType; + userId: string; + cardId?: number; + commentId?: number; + workspaceId?: number; + metadata?: string; + }, +) => { + const [result] = await db + .insert(notifications) + .values({ + publicId: generateUID(), + type: notificationInput.type, + userId: notificationInput.userId, + cardId: notificationInput.cardId, + commentId: notificationInput.commentId, + workspaceId: notificationInput.workspaceId, + metadata: notificationInput.metadata, + }) + .returning(); + + return result; +}; + +export const exists = async ( + db: dbClient, + args: { + userId: string; + type: NotificationType; + cardId?: number; + workspaceId?: number; + commentId?: number; + }, +) => { + const result = await db.query.notifications.findFirst({ + where: (notifications, { eq, and, isNull: isNullFn }) => { + const conditions = [ + eq(notifications.userId, args.userId), + eq(notifications.type, args.type), + isNullFn(notifications.deletedAt), + ]; + + if (args.cardId) { + conditions.push(eq(notifications.cardId, args.cardId)); + } + + if (args.workspaceId) { + conditions.push(eq(notifications.workspaceId, args.workspaceId)); + } + + return and(...conditions); + }, + }); + + return !!result; +}; + +export const markAsRead = async ( + db: dbClient, + notificationId: number, +) => { + const [result] = await db + .update(notifications) + .set({ readAt: new Date() }) + .where(eq(notifications.id, notificationId)) + .returning(); + + return result; +}; + +export const getUnreadCount = async ( + db: dbClient, + userId: string, +) => { + const result = await db + .select({ count: count() }) + .from(notifications) + .where( + and( + eq(notifications.userId, userId), + isNull(notifications.readAt), + isNull(notifications.deletedAt), + ), + ); + + return result[0]?.count ?? 0; +}; + diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 0162cf55..b72f722a 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -13,3 +13,4 @@ export * from "./workspaces"; export * from "./subscriptions"; export * from "./workspaceInviteLinks"; export * from "./permissions"; +export * from "./notifications"; diff --git a/packages/db/src/schema/notifications.ts b/packages/db/src/schema/notifications.ts new file mode 100644 index 00000000..cc3437f9 --- /dev/null +++ b/packages/db/src/schema/notifications.ts @@ -0,0 +1,98 @@ +import { relations } from "drizzle-orm"; +import { + bigint, + bigserial, + index, + pgEnum, + pgTable, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; + +import { cards } from "./cards"; +import { comments } from "./cards"; +import { users } from "./users"; +import { workspaces } from "./workspaces"; + +export const notificationTypes = [ + "mention", + "workspace.member.added", + "workspace.member.removed", + "workspace.role.changed", +] as const; + +export type NotificationType = (typeof notificationTypes)[number]; + +export const notificationTypeEnum = pgEnum("notification_type", notificationTypes); + +export const notifications = pgTable( + "notification", + { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + type: notificationTypeEnum("type").notNull(), + userId: uuid("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + cardId: bigint("cardId", { mode: "number" }).references(() => cards.id, { + onDelete: "cascade", + }), + commentId: bigint("commentId", { mode: "number" }).references( + () => comments.id, + { onDelete: "cascade" }, + ), + workspaceId: bigint("workspaceId", { mode: "number" }).references( + () => workspaces.id, + { onDelete: "cascade" }, + ), + metadata: text("metadata"), + readAt: timestamp("readAt"), + createdAt: timestamp("createdAt").defaultNow().notNull(), + deletedAt: timestamp("deletedAt"), + }, + (table) => [ + index("notification_user_deleted_idx").on(table.userId, table.deletedAt), + index("notification_user_read_deleted_idx").on( + table.userId, + table.readAt, + table.deletedAt, + ), + index("notification_user_type_card_idx").on( + table.userId, + table.type, + table.cardId, + ), + index("notification_user_type_workspace_idx").on( + table.userId, + table.type, + table.workspaceId, + ), + index("notification_user_created_idx").on(table.userId, table.createdAt), + ], +).enableRLS(); + +export const notificationsRelations = relations(notifications, ({ one }) => ({ + user: one(users, { + fields: [notifications.userId], + references: [users.id], + relationName: "notificationsUser", + }), + card: one(cards, { + fields: [notifications.cardId], + references: [cards.id], + relationName: "notificationsCard", + }), + comment: one(comments, { + fields: [notifications.commentId], + references: [comments.id], + relationName: "notificationsComment", + }), + workspace: one(workspaces, { + fields: [notifications.workspaceId], + references: [workspaces.id], + relationName: "notificationsWorkspace", + }), +})); + diff --git a/packages/email/src/sendEmail.tsx b/packages/email/src/sendEmail.tsx index a087606c..1a696c62 100644 --- a/packages/email/src/sendEmail.tsx +++ b/packages/email/src/sendEmail.tsx @@ -3,14 +3,16 @@ import nodemailer from "nodemailer"; import JoinWorkspaceTemplate from "./templates/join-workspace"; import MagicLinkTemplate from "./templates/magic-link"; +import MentionTemplate from "./templates/mention"; import ResetPasswordTemplate from "./templates/reset-password"; -type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD"; +type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD" | "MENTION"; -const emailTemplates: Record = { +const emailTemplates: Record> = { MAGIC_LINK: MagicLinkTemplate, JOIN_WORKSPACE: JoinWorkspaceTemplate, RESET_PASSWORD: ResetPasswordTemplate, + MENTION: MentionTemplate, }; const transporter = nodemailer.createTransport({ diff --git a/packages/email/src/templates/mention.tsx b/packages/email/src/templates/mention.tsx new file mode 100644 index 00000000..5d97760f --- /dev/null +++ b/packages/email/src/templates/mention.tsx @@ -0,0 +1,108 @@ +import { Body } from "@react-email/body"; +import { Button } from "@react-email/button"; +import { Container } from "@react-email/container"; +import { Head } from "@react-email/head"; +import { Heading } from "@react-email/heading"; +import { Hr } from "@react-email/hr"; +import { Html } from "@react-email/html"; +import { Link } from "@react-email/link"; +import { Preview } from "@react-email/preview"; +import { Text } from "@react-email/text"; +import { env } from "next-runtime-env"; +import * as React from "react"; + +export const MentionTemplate = ({ + commenterName, + boardName, + cardTitle, + cardUrl, +}: { + commenterName: string; + boardName: string; + cardTitle: string; + cardUrl: string; +}) => ( + + + + {commenterName} mentioned you in a comment on {cardTitle} + + + + + kan.bn + + + You were mentioned in a comment + + + {commenterName} mentioned you in a comment on the + card {cardTitle} in the board {boardName}. + + +
+ + + Kan + + , the open source Trello alternative. + +
+ + +); + +export default MentionTemplate; + diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 0e6ab7d1..28c63eb6 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -4,3 +4,4 @@ export * from "./subscriptions"; export * from "./email"; export * from "./dueDateFilters"; export * from "./s3"; +export * from "./mentions"; diff --git a/packages/shared/src/utils/mentions.ts b/packages/shared/src/utils/mentions.ts new file mode 100644 index 00000000..42f16088 --- /dev/null +++ b/packages/shared/src/utils/mentions.ts @@ -0,0 +1,22 @@ +/** + * Parses mention data-id attributes from HTML content + * Mentions are stored as: @label + * @param htmlContent - The HTML content to parse + * @returns Array of unique mention public IDs + */ +export function parseMentionsFromHTML(htmlContent: string): string[] { + if (!htmlContent) return []; + + // Match all mention spans with data-id attributes + const mentionRegex = /]*data-type="mention"[^>]*data-id="([^"]+)"[^>]*>/gi; + const matches = Array.from(htmlContent.matchAll(mentionRegex)); + + // Extract unique mention IDs + const mentionIds = matches + .map((match) => match[1]) + .filter((id): id is string => !!id && id.length >= 12); + + // Return unique IDs + return Array.from(new Set(mentionIds)); +} +