refactor: reinstate drizzle

This commit is contained in:
Henry
2025-04-23 13:11:00 +01:00
parent 0c25fe3864
commit c1348cf455
22 changed files with 1964 additions and 879 deletions

View File

@@ -57,18 +57,11 @@ export const cardRouter = createTRPCRouter({
let index = 0; let index = 0;
if (list.cards.length) { if (list.cards.length && input.position === "end" && lastCard) {
if (input.position === "end" && lastCard) index = lastCard.index + 1; index = lastCard.index + 1;
if (input.position === "start") {
await cardRepo.pushIndex(ctx.db, {
listId: list.id,
cardIndex: 0,
});
}
} }
const newCard = await cardRepo.create(ctx.db, { const newCard = await cardRepo.create(ctx.drizzleDb, {
title: input.title, title: input.title,
description: input.description, description: input.description,
createdBy: userId, createdBy: userId,
@@ -76,7 +69,7 @@ export const cardRouter = createTRPCRouter({
index, index,
}); });
const newCardId = newCard?.id; const newCardId = newCard.id;
if (!newCardId) if (!newCardId)
throw new TRPCError({ throw new TRPCError({
@@ -84,12 +77,6 @@ export const cardRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
}); });
await cardActivityRepo.create(ctx.db, {
type: "card.created",
cardId: newCard.id,
createdBy: userId,
});
if (newCardId && input.labelPublicIds.length) { if (newCardId && input.labelPublicIds.length) {
const labels = await labelRepo.getAllByPublicIds( const labels = await labelRepo.getAllByPublicIds(
ctx.db, ctx.db,

View File

@@ -24,7 +24,7 @@ export const feedbackRouter = createTRPCRouter({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
}); });
const result = await feedbackRepo.create(ctx.adminDb, { const result = await feedbackRepo.create(ctx.db, {
feedback: input.feedback, feedback: input.feedback,
createdBy: userId, createdBy: userId,
url: input.url, url: input.url,

View File

@@ -73,12 +73,12 @@ export const memberRouter = createTRPCRouter({
let hashedToken: string | undefined; let hashedToken: string | undefined;
let verificationType: string | undefined; let verificationType: string | undefined;
const existingUser = await userRepo.getByEmail(ctx.adminDb, input.email); const existingUser = await userRepo.getByEmail(ctx.db, input.email);
if (existingUser) { if (existingUser) {
invitedUserId = existingUser.id; invitedUserId = existingUser.id;
const magicLink = await ctx.adminDb.auth.admin.generateLink({ const magicLink = await ctx.db.auth.admin.generateLink({
type: "magiclink", type: "magiclink",
email: input.email, email: input.email,
options: { options: {
@@ -89,7 +89,7 @@ export const memberRouter = createTRPCRouter({
hashedToken = magicLink.data.properties?.hashed_token; hashedToken = magicLink.data.properties?.hashed_token;
verificationType = magicLink.data.properties?.verification_type; verificationType = magicLink.data.properties?.verification_type;
} else { } else {
const invite = await ctx.adminDb.auth.admin.generateLink({ const invite = await ctx.db.auth.admin.generateLink({
type: "invite", type: "invite",
email: input.email, email: input.email,
options: { options: {
@@ -111,7 +111,7 @@ export const memberRouter = createTRPCRouter({
}, },
}); });
const newUser = await userRepo.create(ctx.adminDb, { const newUser = await userRepo.create(ctx.db, {
email: invitedUserEmail, email: invitedUserEmail,
id: invitedUserAuthId, id: invitedUserAuthId,
stripeCustomerId: stripeCustomer.id, stripeCustomerId: stripeCustomer.id,

View File

@@ -37,7 +37,7 @@ export const userRouter = createTRPCRouter({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
}); });
const result = await userRepo.getById(ctx.db, userId); const result = await userRepo.getById(ctx.drizzleDb, userId);
if (!result?.name) { if (!result?.name) {
throw new TRPCError({ throw new TRPCError({
@@ -81,7 +81,7 @@ export const userRouter = createTRPCRouter({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
}); });
const result = await userRepo.update(ctx.adminDb, userId, input); const result = await userRepo.update(ctx.db, userId, input);
if (!result) { if (!result) {
throw new TRPCError({ throw new TRPCError({

View File

@@ -126,7 +126,7 @@ export const workspaceRouter = createTRPCRouter({
const workspacePublicId = generateUID(); const workspacePublicId = generateUID();
const result = await workspaceRepo.create(ctx.adminDb, { const result = await workspaceRepo.create(ctx.db, {
publicId: workspacePublicId, publicId: workspacePublicId,
name: input.name, name: input.name,
slug: workspacePublicId, slug: workspacePublicId,
@@ -174,7 +174,7 @@ export const workspaceRouter = createTRPCRouter({
); );
const reservedOrPremiumWorkspaceSlug = const reservedOrPremiumWorkspaceSlug =
await workspaceSlugRepo.getWorkspaceSlug(ctx.adminDb, input.slug); await workspaceSlugRepo.getWorkspaceSlug(ctx.db, input.slug);
const isWorkspaceSlugAvailable = const isWorkspaceSlugAvailable =
await workspaceRepo.isWorkspaceSlugAvailable(ctx.db, input.slug); await workspaceRepo.isWorkspaceSlugAvailable(ctx.db, input.slug);
@@ -261,7 +261,7 @@ export const workspaceRouter = createTRPCRouter({
const slug = input.workspaceSlug.toLowerCase(); const slug = input.workspaceSlug.toLowerCase();
// check slug is not reserved // check slug is not reserved
const workspaceSlug = await workspaceSlugRepo.getWorkspaceSlug( const workspaceSlug = await workspaceSlugRepo.getWorkspaceSlug(
ctx.adminDb, ctx.db,
slug, slug,
); );

View File

@@ -1,20 +1,15 @@
// import type { Pool } from "@neondatabase/serverless";
import type { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch"; import type { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
// import type { NeonDatabase as DrizzleClient } from "drizzle-orm/neon-serverless";
import type { OpenApiMeta } from "trpc-to-openapi"; import type { OpenApiMeta } from "trpc-to-openapi";
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson"; import superjson from "superjson";
import { ZodError } from "zod"; import { ZodError } from "zod";
import type { dbClient } from "@kan/db/client";
import type { Database } from "@kan/db/types/database.types"; import type { Database } from "@kan/db/types/database.types";
import type { SupabaseClient } from "@kan/supabase"; import type { SupabaseClient } from "@kan/supabase";
// import { createDrizzleClient } from "@kan/db/client"; import { createDrizzleClient } from "@kan/db/client";
import { import { createNextApiClient, createTRPCClient } from "@kan/supabase";
createNextApiClient,
createTRPCAdminClient,
createTRPCClient,
} from "@kan/supabase";
export interface User { export interface User {
id: string; id: string;
@@ -23,22 +18,14 @@ export interface User {
interface CreateContextOptions { interface CreateContextOptions {
user: User | null; user: User | null;
db: SupabaseClient<Database>; db: SupabaseClient<Database>;
adminDb: SupabaseClient<Database>; drizzleDb: dbClient;
// drizzleDb:
// | (DrizzleClient<
// typeof import("/Users/henryball/kan/packages/db/dist/schema")
// > & {
// $client: Pool;
// })
// | null;
} }
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
user: opts.user, user: opts.user,
db: opts.db, db: opts.db,
adminDb: opts.adminDb, drizzleDb: opts.drizzleDb,
// drizzleDb: opts.drizzleDb,
}; };
}; };
@@ -47,37 +34,35 @@ export const createTRPCContext = async ({
resHeaders, resHeaders,
}: FetchCreateContextFnOptions) => { }: FetchCreateContextFnOptions) => {
const db = createTRPCClient(req, resHeaders); const db = createTRPCClient(req, resHeaders);
const adminDb = createTRPCAdminClient();
const { const {
data: { user }, data: { user },
} = await db.auth.getUser(); } = await db.auth.getUser();
// const drizzleDb = createDrizzleClient(); const drizzleDb = createDrizzleClient();
return createInnerTRPCContext({ db, adminDb, user }); return createInnerTRPCContext({ db, user, drizzleDb });
}; };
export const createRESTContext = async ({ req }: CreateNextContextOptions) => { export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
const db = createNextApiClient(req); const db = createNextApiClient(req);
const adminDb = createTRPCAdminClient();
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
const accessToken = authHeader?.startsWith("Bearer ") const accessToken = authHeader?.startsWith("Bearer ")
? authHeader.substring(7) ? authHeader.substring(7)
: null; : null;
const drizzleDb = createDrizzleClient();
if (!accessToken) { if (!accessToken) {
return createInnerTRPCContext({ db, adminDb, user: null }); return createInnerTRPCContext({ db, user: null, drizzleDb });
} }
const { const {
data: { user }, data: { user },
} = await db.auth.getUser(accessToken); } = await db.auth.getUser(accessToken);
// const drizzleDb = createDrizzleClient(); return createInnerTRPCContext({ db, user, drizzleDb });
return createInnerTRPCContext({ db, adminDb, user });
}; };
const t = initTRPC const t = initTRPC

View File

@@ -0,0 +1,37 @@
DROP POLICY "Allow access to boards in user's workspace or public boards" ON "board" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting boards in user's workspace" ON "board" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating boards in user's workspace" ON "board" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting boards in user's workspace" ON "board" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to card activity in user's workspace or public boards" ON "card_activity" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting card activity in user's workspace" ON "card_activity" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to card workspace members in user's workspace" ON "_card_workspace_members" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to cards in user's workspace or public boards" ON "card" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting cards in user's workspace" ON "card" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating cards in user's workspace" ON "card" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting cards in user's workspace" ON "card" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to card labels in user's workspace or public boards" ON "_card_labels" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting card labels in user's workspace" ON "_card_labels" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating card labels in user's workspace" ON "_card_labels" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting card labels in user's workspace" ON "_card_labels" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to card comments in user's workspace or public boards" ON "card_comments" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting comments on cards in user's workspace" ON "card_comments" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating own comments" ON "card_comments" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting own comments" ON "card_comments" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to user's own imports" ON "import" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to labels in user's workspace or public boards" ON "label" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting labels in user's workspace" ON "label" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating labels in user's workspace" ON "label" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting labels in user's workspace" ON "label" CASCADE;--> statement-breakpoint
DROP POLICY "Allow access to lists in user's workspace or public boards" ON "list" CASCADE;--> statement-breakpoint
DROP POLICY "Allow inserting lists in user's workspace" ON "list" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating lists in user's workspace" ON "list" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting lists in user's workspace" ON "list" CASCADE;--> statement-breakpoint
DROP POLICY "Allow viewing members in user's workspace" ON "user" CASCADE;--> statement-breakpoint
DROP POLICY "Allow members to view workspace membership" ON "workspace_members" CASCADE;--> statement-breakpoint
DROP POLICY "Allow admins to add workspace members" ON "workspace_members" CASCADE;--> statement-breakpoint
DROP POLICY "Allow admins to update workspace members" ON "workspace_members" CASCADE;--> statement-breakpoint
DROP POLICY "Allow admins to remove workspace members" ON "workspace_members" CASCADE;--> statement-breakpoint
DROP POLICY "Allow viewing user's workspaces" ON "workspace" CASCADE;--> statement-breakpoint
DROP POLICY "Allow updating user's workspaces" ON "workspace" CASCADE;--> statement-breakpoint
DROP POLICY "Allow deleting user's workspaces" ON "workspace" CASCADE;--> statement-breakpoint
DROP POLICY "Allow authenticated users to create workspaces" ON "workspace" CASCADE;

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,13 @@
"when": 1744906695652, "when": 1744906695652,
"tag": "0008_mature_ravenous", "tag": "0008_mature_ravenous",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1745407291997,
"tag": "0009_shallow_silver_surfer",
"breakpoints": true
} }
] ]
} }

View File

@@ -41,7 +41,7 @@
"@kan/shared": "workspace:^", "@kan/shared": "workspace:^",
"@neondatabase/serverless": "^0.10.4", "@neondatabase/serverless": "^0.10.4",
"@vercel/postgres": "^0.10.0", "@vercel/postgres": "^0.10.0",
"drizzle-orm": "^0.36.4", "drizzle-orm": "^0.42.0",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"zod": "catalog:" "zod": "catalog:"
}, },

View File

@@ -1,8 +1,13 @@
import type { NeonDatabase as DrizzleClient } from "drizzle-orm/neon-serverless";
import { Pool } from "@neondatabase/serverless"; import { Pool } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-serverless"; import { drizzle } from "drizzle-orm/neon-serverless";
import * as schema from "./schema"; import * as schema from "./schema";
export type dbClient = DrizzleClient<typeof schema> & {
$client: Pool;
};
export const createDrizzleClient = () => { export const createDrizzleClient = () => {
const pool = new Pool({ const pool = new Pool({
connectionString: process.env.POSTGRES_URL, connectionString: process.env.POSTGRES_URL,

View File

@@ -1,10 +1,13 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import { and, eq, isNull, sql } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import type { Database } from "@kan/db/types/database.types"; import type { Database } from "@kan/db/types/database.types";
import * as schema from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
cardInput: { cardInput: {
title: string; title: string;
description: string; description: string;
@@ -13,21 +16,56 @@ export const create = async (
index: number; index: number;
}, },
) => { ) => {
const { data } = await db return db.transaction(async (tx) => {
.from("card") const getExistingCardAtIndex = async () =>
.insert({ tx.query.cards.findFirst({
publicId: generateUID(), columns: {
title: cardInput.title, id: true,
description: cardInput.description, },
createdBy: cardInput.createdBy, where: and(
listId: cardInput.listId, eq(schema.cards.listId, cardInput.listId),
index: cardInput.index, eq(schema.cards.index, cardInput.index),
}) isNull(schema.cards.deletedAt),
.select(`id`) ),
.limit(1) });
.single();
return data; const existingCardAtIndex = await getExistingCardAtIndex();
if (existingCardAtIndex?.id) {
await tx.execute(sql`
UPDATE card
SET index = index + 1
WHERE "listId" = ${cardInput.listId} AND index >= ${cardInput.index} AND "deletedAt" IS NULL;
`);
const refetchedExistingCardAtIndex = await getExistingCardAtIndex();
if (refetchedExistingCardAtIndex?.id) return tx.rollback();
}
const result = await tx
.insert(schema.cards)
.values({
publicId: generateUID(),
title: cardInput.title,
description: cardInput.description,
createdBy: cardInput.createdBy,
listId: cardInput.listId,
index: cardInput.index,
})
.returning({ id: schema.cards.id });
if (!result[0]) return tx.rollback();
await tx.insert(schema.cardActivities).values({
publicId: generateUID(),
cardId: result[0].id,
type: "card.created",
createdBy: cardInput.createdBy,
});
return result[0];
});
}; };
export const bulkCreateCardLabelRelationships = async ( export const bulkCreateCardLabelRelationships = async (

View File

@@ -1,14 +1,21 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import { eq } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import type { Database } from "@kan/db/types/database.types"; import type { Database } from "@kan/db/types/database.types";
import * as schema from "@kan/db/schema";
export const getById = async (db: SupabaseClient<Database>, userId: string) => { export const getById = async (db: dbClient, userId: string) => {
const { data } = await db const data = await db.query.users.findFirst({
.from("user") columns: {
.select(`id, name, email, image, stripeCustomerId`) id: true,
.eq("id", userId) name: true,
.limit(1) email: true,
.single(); image: true,
stripeCustomerId: true,
},
where: eq(schema.users.id, userId),
});
return data; return data;
}; };

View File

@@ -4,7 +4,6 @@ import {
bigserial, bigserial,
index, index,
pgEnum, pgEnum,
pgPolicy,
pgTable, pgTable,
text, text,
timestamp, timestamp,
@@ -12,7 +11,6 @@ import {
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { anonRole, authenticatedRole } from "drizzle-orm/supabase";
import { imports } from "./imports"; import { imports } from "./imports";
import { labels } from "./labels"; import { labels } from "./labels";
@@ -53,55 +51,6 @@ export const boards = pgTable(
uniqueIndex("unique_slug_per_workspace") uniqueIndex("unique_slug_per_workspace")
.on(table.workspaceId, table.slug) .on(table.workspaceId, table.slug)
.where(sql`${table.deletedAt} IS NULL`), .where(sql`${table.deletedAt} IS NULL`),
pgPolicy("Allow access to boards in user's workspace or public boards", {
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
OR visibility = 'public'
`,
}),
pgPolicy("Allow inserting boards in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating boards in user's workspace", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting boards in user's workspace", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
`,
}),
], ],
).enableRLS(); ).enableRLS();

View File

@@ -1,10 +1,9 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint, bigint,
bigserial, bigserial,
integer, integer,
pgEnum, pgEnum,
pgPolicy,
pgTable, pgTable,
primaryKey, primaryKey,
text, text,
@@ -12,7 +11,6 @@ import {
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { anonRole, authenticatedRole } from "drizzle-orm/supabase";
import { imports } from "./imports"; import { imports } from "./imports";
import { labels } from "./labels"; import { labels } from "./labels";
@@ -36,89 +34,24 @@ export const activityTypeEnum = pgEnum("card_activity_type", [
"card.archived", "card.archived",
]); ]);
export const cards = pgTable( export const cards = pgTable("card", {
"card", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), title: varchar("title", { length: 255 }).notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), description: text("description"),
title: varchar("title", { length: 255 }).notNull(), index: integer("index").notNull(),
description: text("description"), createdBy: uuid("createdBy")
index: integer("index").notNull(), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), deletedAt: timestamp("deletedAt"),
updatedAt: timestamp("updatedAt"), deletedBy: uuid("deletedBy").references(() => users.id),
deletedAt: timestamp("deletedAt"), listId: bigint("listId", { mode: "number" })
deletedBy: uuid("deletedBy").references(() => users.id), .notNull()
listId: bigint("listId", { mode: "number" }) .references(() => lists.id, { onDelete: "cascade" }),
.notNull() importId: bigint("importId", { mode: "number" }).references(() => imports.id),
.references(() => lists.id, { onDelete: "cascade" }), }).enableRLS();
importId: bigint("importId", { mode: "number" }).references(
() => imports.id,
),
},
() => [
pgPolicy("Allow access to cards in user's workspace or public boards", {
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"listId" IN (
SELECT l.id
FROM list l
JOIN board b ON l."boardId" = b.id
LEFT JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
}),
pgPolicy("Allow inserting cards in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"listId" IN (
SELECT l.id
FROM list l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating cards in user's workspace", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"listId" IN (
SELECT l.id
FROM list l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting cards in user's workspace", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"listId" IN (
SELECT l.id
FROM list l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
],
).enableRLS();
export const cardsRelations = relations(cards, ({ one, many }) => ({ export const cardsRelations = relations(cards, ({ one, many }) => ({
createdBy: one(users, { createdBy: one(users, {
@@ -142,76 +75,37 @@ export const cardsRelations = relations(cards, ({ one, many }) => ({
comments: many(comments), comments: many(comments),
})); }));
export const cardActivities = pgTable( export const cardActivities = pgTable("card_activity", {
"card_activity", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), type: activityTypeEnum("type").notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), cardId: bigint("cardId", { mode: "number" })
type: activityTypeEnum("type").notNull(), .notNull()
cardId: bigint("cardId", { mode: "number" }) .references(() => cards.id, { onDelete: "cascade" }),
.notNull() fromIndex: integer("fromIndex"),
.references(() => cards.id, { onDelete: "cascade" }), toIndex: integer("toIndex"),
fromIndex: integer("fromIndex"), fromListId: bigint("fromListId", { mode: "number" }).references(
toIndex: integer("toIndex"), () => lists.id,
fromListId: bigint("fromListId", { mode: "number" }).references( ),
() => lists.id, toListId: bigint("toListId", { mode: "number" }).references(() => lists.id),
), labelId: bigint("labelId", { mode: "number" }).references(() => labels.id),
toListId: bigint("toListId", { mode: "number" }).references(() => lists.id), workspaceMemberId: bigint("workspaceMemberId", {
labelId: bigint("labelId", { mode: "number" }).references(() => labels.id), mode: "number",
workspaceMemberId: bigint("workspaceMemberId", { }).references(() => workspaceMembers.id),
mode: "number", fromTitle: varchar("fromTitle", { length: 255 }),
}).references(() => workspaceMembers.id), toTitle: varchar("toTitle", { length: 255 }),
fromTitle: varchar("fromTitle", { length: 255 }), fromDescription: text("fromDescription"),
toTitle: varchar("toTitle", { length: 255 }), toDescription: text("toDescription"),
fromDescription: text("fromDescription"), createdBy: uuid("createdBy")
toDescription: text("toDescription"), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), commentId: bigint("commentId", { mode: "number" }).references(
createdAt: timestamp("createdAt").defaultNow().notNull(), () => comments.id,
commentId: bigint("commentId", { mode: "number" }).references( ),
() => comments.id, fromComment: text("fromComment"),
), toComment: text("toComment"),
fromComment: text("fromComment"), }).enableRLS();
toComment: text("toComment"),
},
() => [
pgPolicy(
"Allow access to card activity in user's workspace or public boards",
{
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
},
),
pgPolicy("Allow inserting card activity in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
],
).enableRLS();
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
card: one(cards, { card: one(cards, {
@@ -250,106 +144,7 @@ export const cardsToLabels = pgTable(
.notNull() .notNull()
.references(() => labels.id, { onDelete: "cascade" }), .references(() => labels.id, { onDelete: "cascade" }),
}, },
(t) => [ (t) => [primaryKey({ columns: [t.cardId, t.labelId] })],
primaryKey({ columns: [t.cardId, t.labelId] }),
pgPolicy(
"Allow access to card labels in user's workspace or public boards",
{
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
LEFT JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId" AND wm."userId" = auth.uid()
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
AND
"labelId" IN (
SELECT l.id
FROM label l
JOIN board b ON l."boardId" = b.id
LEFT JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId" AND wm."userId" = auth.uid()
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
},
),
pgPolicy("Allow inserting card labels in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
AND
"labelId" IN (
SELECT l.id
FROM label l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating card labels in user's workspace", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
AND
"labelId" IN (
SELECT l.id
FROM label l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting card labels in user's workspace", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
AND
"labelId" IN (
SELECT l.id
FROM label l
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
],
).enableRLS(); ).enableRLS();
export const cardToLabelsRelations = relations(cardsToLabels, ({ one }) => ({ export const cardToLabelsRelations = relations(cardsToLabels, ({ one }) => ({
@@ -373,34 +168,7 @@ export const cardToWorkspaceMembers = pgTable(
.notNull() .notNull()
.references(() => workspaceMembers.id, { onDelete: "cascade" }), .references(() => workspaceMembers.id, { onDelete: "cascade" }),
}, },
(t) => [ (t) => [primaryKey({ columns: [t.cardId, t.workspaceMemberId] })],
primaryKey({ columns: [t.cardId, t.workspaceMemberId] }),
pgPolicy("Allow access to card workspace members in user's workspace", {
for: "all",
as: "permissive",
to: [authenticatedRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
AND
"workspaceMemberId" IN (
SELECT wm.id
FROM workspace_members wm
WHERE wm."workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
)
`,
}),
],
).enableRLS(); ).enableRLS();
export const cardToWorkspaceMembersRelations = relations( export const cardToWorkspaceMembersRelations = relations(
@@ -417,76 +185,21 @@ export const cardToWorkspaceMembersRelations = relations(
}), }),
); );
export const comments = pgTable( export const comments = pgTable("card_comments", {
"card_comments", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), comment: text("comment").notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), cardId: bigint("cardId", { mode: "number" })
comment: text("comment").notNull(), .notNull()
cardId: bigint("cardId", { mode: "number" }) .references(() => cards.id, { onDelete: "cascade" }),
.notNull() createdBy: uuid("createdBy")
.references(() => cards.id, { onDelete: "cascade" }), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), deletedAt: timestamp("deletedAt"),
updatedAt: timestamp("updatedAt"), deletedBy: uuid("deletedBy").references(() => users.id),
deletedAt: timestamp("deletedAt"), }).enableRLS();
deletedBy: uuid("deletedBy").references(() => users.id),
},
() => [
pgPolicy(
"Allow access to card comments in user's workspace or public boards",
{
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
},
),
pgPolicy("Allow inserting comments on cards in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"cardId" IN (
SELECT c.id
FROM card c
JOIN list l ON c."listId" = l.id
JOIN board b ON l."boardId" = b.id
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating own comments", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"createdBy" = auth.uid()
`,
}),
pgPolicy("Allow deleting own comments", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"createdBy" = auth.uid()
`,
}),
],
).enableRLS();
export const commentsRelations = relations(comments, ({ one }) => ({ export const commentsRelations = relations(comments, ({ one }) => ({
card: one(cards, { card: one(cards, {

View File

@@ -1,14 +1,12 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigserial, bigserial,
pgEnum, pgEnum,
pgPolicy,
pgTable, pgTable,
timestamp, timestamp,
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { authenticatedRole } from "drizzle-orm/supabase";
import { boards } from "./boards"; import { boards } from "./boards";
import { cards } from "./cards"; import { cards } from "./cards";
@@ -23,29 +21,16 @@ export const importStatusEnum = pgEnum("status", [
"failed", "failed",
]); ]);
export const imports = pgTable( export const imports = pgTable("import", {
"import", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), source: importSourceEnum("source").notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), status: importStatusEnum("status").notNull(),
source: importSourceEnum("source").notNull(), createdBy: uuid("createdBy")
status: importStatusEnum("status").notNull(), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), }).enableRLS();
createdAt: timestamp("createdAt").defaultNow().notNull(),
},
() => [
pgPolicy("Allow access to user's own imports", {
for: "all",
as: "permissive",
to: [authenticatedRole],
using: sql`
"createdBy" = auth.uid()
`,
}),
],
).enableRLS();
export const importsRelations = relations(imports, ({ one, many }) => ({ export const importsRelations = relations(imports, ({ one, many }) => ({
createdBy: one(users, { createdBy: one(users, {

View File

@@ -1,95 +1,33 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint, bigint,
bigserial, bigserial,
pgPolicy,
pgTable, pgTable,
timestamp, timestamp,
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { anonRole, authenticatedRole } from "drizzle-orm/supabase";
import { boards } from "./boards"; import { boards } from "./boards";
import { cardsToLabels } from "./cards"; import { cardsToLabels } from "./cards";
import { imports } from "./imports"; import { imports } from "./imports";
import { users } from "./users"; import { users } from "./users";
export const labels = pgTable( export const labels = pgTable("label", {
"label", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), name: varchar("name", { length: 255 }).notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), colourCode: varchar("colourCode", { length: 12 }),
name: varchar("name", { length: 255 }).notNull(), createdBy: uuid("createdBy")
colourCode: varchar("colourCode", { length: 12 }), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), boardId: bigint("boardId", { mode: "number" })
updatedAt: timestamp("updatedAt"), .notNull()
boardId: bigint("boardId", { mode: "number" }) .references(() => boards.id, { onDelete: "cascade" }),
.notNull() importId: bigint("importId", { mode: "number" }).references(() => imports.id),
.references(() => boards.id, { onDelete: "cascade" }), }).enableRLS();
importId: bigint("importId", { mode: "number" }).references(
() => imports.id,
),
},
() => [
pgPolicy("Allow access to labels in user's workspace or public boards", {
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
LEFT JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
}),
pgPolicy("Allow inserting labels in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating labels in user's workspace", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting labels in user's workspace", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
],
).enableRLS();
export const labelsRelations = relations(labels, ({ one, many }) => ({ export const labelsRelations = relations(labels, ({ one, many }) => ({
createdBy: one(users, { createdBy: one(users, {

View File

@@ -1,98 +1,36 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint, bigint,
bigserial, bigserial,
integer, integer,
pgPolicy,
pgTable, pgTable,
timestamp, timestamp,
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { anonRole, authenticatedRole } from "drizzle-orm/supabase";
import { boards } from "./boards"; import { boards } from "./boards";
import { cards } from "./cards"; import { cards } from "./cards";
import { imports } from "./imports"; import { imports } from "./imports";
import { users } from "./users"; import { users } from "./users";
export const lists = pgTable( export const lists = pgTable("list", {
"list", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), name: varchar("name", { length: 255 }).notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), index: integer("index").notNull(),
name: varchar("name", { length: 255 }).notNull(), createdBy: uuid("createdBy")
index: integer("index").notNull(), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), deletedAt: timestamp("deletedAt"),
updatedAt: timestamp("updatedAt"), deletedBy: uuid("deletedBy").references(() => users.id),
deletedAt: timestamp("deletedAt"), boardId: bigint("boardId", { mode: "number" })
deletedBy: uuid("deletedBy").references(() => users.id), .notNull()
boardId: bigint("boardId", { mode: "number" }) .references(() => boards.id, { onDelete: "cascade" }),
.notNull() importId: bigint("importId", { mode: "number" }).references(() => imports.id),
.references(() => boards.id, { onDelete: "cascade" }), }).enableRLS();
importId: bigint("importId", { mode: "number" }).references(
() => imports.id,
),
},
() => [
pgPolicy("Allow access to lists in user's workspace or public boards", {
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
LEFT JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
OR b.visibility = 'public'
)
`,
}),
pgPolicy("Allow inserting lists in user's workspace", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow updating lists in user's workspace", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting lists in user's workspace", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
"boardId" IN (
SELECT b.id
FROM board b
JOIN workspace_members wm ON b."workspaceId" = wm."workspaceId"
WHERE wm."userId" = auth.uid()
)
`,
}),
],
).enableRLS();
export const listsRelations = relations(lists, ({ one, many }) => ({ export const listsRelations = relations(lists, ({ one, many }) => ({
createdBy: one(users, { createdBy: one(users, {

View File

@@ -1,12 +1,5 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
pgPolicy,
pgTable,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { authenticatedRole } from "drizzle-orm/supabase";
import { boards } from "./boards"; import { boards } from "./boards";
import { cards } from "./cards"; import { cards } from "./cards";
@@ -14,35 +7,14 @@ import { imports } from "./imports";
import { lists } from "./lists"; import { lists } from "./lists";
import { workspaceMembers, workspaces } from "./workspaces"; import { workspaceMembers, workspaces } from "./workspaces";
export const users = pgTable( export const users = pgTable("user", {
"user", id: uuid("id").notNull().primaryKey(),
{ name: varchar("name", { length: 255 }),
id: uuid("id").notNull().primaryKey(), email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }), emailVerified: timestamp("emailVerified", { mode: "date" }),
email: varchar("email", { length: 255 }).notNull().unique(), image: varchar("image", { length: 255 }),
emailVerified: timestamp("emailVerified", { mode: "date" }), stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
image: varchar("image", { length: 255 }), }).enableRLS();
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
},
() => [
pgPolicy("Allow viewing members in user's workspace", {
for: "select",
as: "permissive",
to: [authenticatedRole],
using: sql`
id IN (
SELECT wm."userId"
FROM workspace_members wm
WHERE wm."workspaceId" IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
)
`,
}),
],
).enableRLS();
export const usersRelations = relations(users, ({ many }) => ({ export const usersRelations = relations(users, ({ many }) => ({
boards: many(boards), boards: many(boards),

View File

@@ -1,16 +1,14 @@
import { relations, sql } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint, bigint,
bigserial, bigserial,
pgEnum, pgEnum,
pgPolicy,
pgTable, pgTable,
text, text,
timestamp, timestamp,
uuid, uuid,
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { anonRole, authenticatedRole } from "drizzle-orm/supabase";
import { users } from "./users"; import { users } from "./users";
@@ -27,140 +25,44 @@ export const workspacePlanEnum = pgEnum("workspace_plan", [
"enterprise", "enterprise",
]); ]);
export const workspaces = pgTable( export const workspaces = pgTable("workspace", {
"workspace", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), name: varchar("name", { length: 255 }).notNull(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(), description: text("description"),
name: varchar("name", { length: 255 }).notNull(), slug: varchar("slug", { length: 255 }).notNull().unique(),
description: text("description"), plan: workspacePlanEnum("plan").notNull().default("free"),
slug: varchar("slug", { length: 255 }).notNull().unique(), createdBy: uuid("createdBy")
plan: workspacePlanEnum("plan").notNull().default("free"), .notNull()
createdBy: uuid("createdBy") .references(() => users.id),
.notNull() createdAt: timestamp("createdAt").defaultNow().notNull(),
.references(() => users.id), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), deletedAt: timestamp("deletedAt"),
updatedAt: timestamp("updatedAt"), deletedBy: uuid("deletedBy").references(() => users.id),
deletedAt: timestamp("deletedAt"), }).enableRLS();
deletedBy: uuid("deletedBy").references(() => users.id),
},
() => [
pgPolicy("Allow viewing user's workspaces", {
for: "select",
as: "permissive",
to: [authenticatedRole, anonRole],
using: sql`
CASE
WHEN auth.uid() IS NULL THEN
EXISTS (
SELECT 1
FROM board
WHERE "workspaceId" = workspace.id
AND visibility = 'public'
)
ELSE
id IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
OR "createdBy" = auth.uid()
END
`,
}),
pgPolicy("Allow updating user's workspaces", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
id IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
`,
}),
pgPolicy("Allow deleting user's workspaces", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
id IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
`,
}),
pgPolicy("Allow authenticated users to create workspaces", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`true`,
}),
],
).enableRLS();
export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
user: one(users, { fields: [workspaces.createdBy], references: [users.id] }), user: one(users, { fields: [workspaces.createdBy], references: [users.id] }),
members: many(workspaceMembers), members: many(workspaceMembers),
})); }));
export const workspaceMembers = pgTable( export const workspaceMembers = pgTable("workspace_members", {
"workspace_members", id: bigserial("id", { mode: "number" }).primaryKey(),
{ publicId: varchar("publicId", { length: 12 }).notNull().unique(),
id: bigserial("id", { mode: "number" }).primaryKey(), userId: uuid("userId")
publicId: varchar("publicId", { length: 12 }).notNull().unique(), .notNull()
userId: uuid("userId") .references(() => users.id),
.notNull() workspaceId: bigint("workspaceId", { mode: "number" })
.references(() => users.id), .notNull()
workspaceId: bigint("workspaceId", { mode: "number" }) .references(() => workspaces.id, { onDelete: "cascade" }),
.notNull() createdBy: uuid("createdBy").notNull(),
.references(() => workspaces.id, { onDelete: "cascade" }), createdAt: timestamp("createdAt").defaultNow().notNull(),
createdBy: uuid("createdBy").notNull(), updatedAt: timestamp("updatedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(), deletedAt: timestamp("deletedAt"),
updatedAt: timestamp("updatedAt"), deletedBy: uuid("deletedBy").references(() => users.id),
deletedAt: timestamp("deletedAt"), role: memberRoleEnum("role").notNull(),
deletedBy: uuid("deletedBy").references(() => users.id), status: memberStatusEnum("status").default("invited").notNull(),
role: memberRoleEnum("role").notNull(), }).enableRLS();
status: memberStatusEnum("status").default("invited").notNull(),
},
() => [
pgPolicy("Allow members to view workspace membership", {
for: "select",
as: "permissive",
to: [authenticatedRole],
using: sql`
"userId" = auth.uid() OR
is_workspace_member(auth.uid(), "workspaceId")
`,
}),
pgPolicy("Allow admins to add workspace members", {
for: "insert",
as: "permissive",
to: [authenticatedRole],
withCheck: sql`
is_workspace_admin(auth.uid(), "workspaceId")
`,
}),
pgPolicy("Allow admins to update workspace members", {
for: "update",
as: "permissive",
to: [authenticatedRole],
using: sql`
is_workspace_admin(auth.uid(), "workspaceId")
`,
}),
pgPolicy("Allow admins to remove workspace members", {
for: "delete",
as: "permissive",
to: [authenticatedRole],
using: sql`
is_workspace_admin(auth.uid(), "workspaceId")
`,
}),
],
).enableRLS();
export const slugs = pgTable("workspace_slugs", { export const slugs = pgTable("workspace_slugs", {
slug: varchar("slug", { length: 255 }).notNull().unique(), slug: varchar("slug", { length: 255 }).notNull().unique(),

View File

@@ -74,15 +74,15 @@ export function createNextApiClient(req: NextApiRequest) {
export function createTRPCClient(req: Request, resHeaders: Headers) { export function createTRPCClient(req: Request, resHeaders: Headers) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const serviceKey = process.env.SUPABASE_SERVICE_API_KEY;
if (!supabaseUrl || !supabaseKey) { if (!supabaseUrl || !serviceKey) {
throw new Error("Missing Supabase environment variables"); throw new Error("Missing Supabase environment variables");
} }
const supabase = createServerClient<Database, "public">( const supabase = createServerClient<Database, "public">(
supabaseUrl, supabaseUrl,
supabaseKey, serviceKey,
{ {
cookies: { cookies: {
get(name: string) { get(name: string) {
@@ -101,27 +101,3 @@ export function createTRPCClient(req: Request, resHeaders: Headers) {
return supabase; return supabase;
} }
export function createTRPCAdminClient() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceKey = process.env.SUPABASE_SERVICE_API_KEY;
if (!supabaseUrl || !serviceKey) {
throw new Error("Missing Supabase environment variables");
}
const supabase = createServerClient<Database, "public">(
supabaseUrl,
serviceKey,
{
cookies: {
get: (_name: string) => "",
set: (_name: string, _value: string, _options: CookieOptions) =>
undefined,
remove: (_name: string, _options: CookieOptions) => undefined,
},
},
);
return supabase;
}

31
pnpm-lock.yaml generated
View File

@@ -264,11 +264,11 @@ importers:
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0(utf-8-validate@6.0.3) version: 0.10.0(utf-8-validate@6.0.3)
drizzle-orm: drizzle-orm:
specifier: ^0.36.4 specifier: ^0.42.0
version: 0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@types/react@18.3.15)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)(react@18.3.1) version: 0.42.0(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)
drizzle-zod: drizzle-zod:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1(drizzle-orm@0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@types/react@18.3.15)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)(react@18.3.1))(zod@3.24.0) version: 0.5.1(drizzle-orm@0.42.0(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5))(zod@3.24.0)
zod: zod:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.24.0 version: 3.24.0
@@ -2562,36 +2562,35 @@ packages:
resolution: {integrity: sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==} resolution: {integrity: sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==}
hasBin: true hasBin: true
drizzle-orm@0.36.4: drizzle-orm@0.42.0:
resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} resolution: {integrity: sha512-pS8nNJm2kBNZwrOjTHJfdKkaU+KuUQmV/vk5D57NojDq4FG+0uAYGMulXtYT///HfgsMF0hnFFvu1ezI3OwOkg==}
peerDependencies: peerDependencies:
'@aws-sdk/client-rds-data': '>=3' '@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=3' '@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0' '@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0' '@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0' '@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0' '@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2' '@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1 '@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1' '@planetscale/database': '>=1.13'
'@prisma/client': '*' '@prisma/client': '*'
'@tidbcloud/serverless': '*' '@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*' '@types/better-sqlite3': '*'
'@types/pg': '*' '@types/pg': '*'
'@types/react': '>=18'
'@types/sql.js': '*' '@types/sql.js': '*'
'@vercel/postgres': '>=0.8.0' '@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*' '@xata.io/client': '*'
better-sqlite3: '>=7' better-sqlite3: '>=7'
bun-types: '*' bun-types: '*'
expo-sqlite: '>=14.0.0' expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*' knex: '*'
kysely: '*' kysely: '*'
mysql2: '>=2' mysql2: '>=2'
pg: '>=8' pg: '>=8'
postgres: '>=3' postgres: '>=3'
prisma: '*' prisma: '*'
react: '>=18'
sql.js: '>=1' sql.js: '>=1'
sqlite3: '>=5' sqlite3: '>=5'
peerDependenciesMeta: peerDependenciesMeta:
@@ -2621,8 +2620,6 @@ packages:
optional: true optional: true
'@types/pg': '@types/pg':
optional: true optional: true
'@types/react':
optional: true
'@types/sql.js': '@types/sql.js':
optional: true optional: true
'@vercel/postgres': '@vercel/postgres':
@@ -2635,6 +2632,8 @@ packages:
optional: true optional: true
expo-sqlite: expo-sqlite:
optional: true optional: true
gel:
optional: true
knex: knex:
optional: true optional: true
kysely: kysely:
@@ -2647,8 +2646,6 @@ packages:
optional: true optional: true
prisma: prisma:
optional: true optional: true
react:
optional: true
sql.js: sql.js:
optional: true optional: true
sqlite3: sqlite3:
@@ -7231,19 +7228,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@types/react@18.3.15)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)(react@18.3.1): drizzle-orm@0.42.0(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5):
optionalDependencies: optionalDependencies:
'@neondatabase/serverless': 0.10.4 '@neondatabase/serverless': 0.10.4
'@types/pg': 8.11.6 '@types/pg': 8.11.6
'@types/react': 18.3.15
'@vercel/postgres': 0.10.0(utf-8-validate@6.0.3) '@vercel/postgres': 0.10.0(utf-8-validate@6.0.3)
pg: 8.13.1 pg: 8.13.1
postgres: 3.4.5 postgres: 3.4.5
react: 18.3.1
drizzle-zod@0.5.1(drizzle-orm@0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@types/react@18.3.15)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)(react@18.3.1))(zod@3.24.0): drizzle-zod@0.5.1(drizzle-orm@0.42.0(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5))(zod@3.24.0):
dependencies: dependencies:
drizzle-orm: 0.36.4(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@types/react@18.3.15)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)(react@18.3.1) drizzle-orm: 0.42.0(@neondatabase/serverless@0.10.4)(@types/pg@8.11.6)(@vercel/postgres@0.10.0(utf-8-validate@6.0.3))(pg@8.13.1)(postgres@3.4.5)
zod: 3.24.0 zod: 3.24.0
dunder-proto@1.0.0: dunder-proto@1.0.0: