Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/lib/userProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use server";
import { prisma } from "@/lib/prisma";

async function createUserProject(userId: string, projectId: string) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed none of the four functions in this file are actually exported yet, so as merged this is dead code and not reachable — which reads like this might genuinely be the "data layer first, action layer later" pattern. If that's the plan, just want to flag it explicitly: whenever export gets added here (which the PR title implies is coming), it'll need a caller-identity + authority check attached at the same time, since UserProject is the authorization-scoping table for the whole system — an unauthenticated grant/revoke here is more consequential than on the other open PRs. Also worth scoping getUserProjects (line 22) to the caller once it's exposed — an empty {} filter currently returns every membership across all projects.

const existing = await prisma.userProject.findUnique({ where: { userId_projectId: { userId, projectId, deletedAt: null } }});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deletedAt: null is nested inside the compound-key object here, but Prisma's generated UserProjectUserIdProjectIdCompoundUniqueInput type only has userId/projectId — running tsc --noEmit against this branch fails here (and at the matching spots in getUserProject/removeUserFromProject) with TS2353: Object literal may only specify known properties. This one's independent of anything else in the review — npm run build won't pass as-is. The fix is to move deletedAt: null up a level, alongside the compound key (where: { userId_projectId: { userId, projectId }, deletedAt: null }), not to just delete it — deleting it would make a soft-deleted membership permanently block re-adding that user (see the upsert note below).

if (existing) {
throw new Error("User project already exists");
}
return await prisma.userProject.upsert({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the type error above is fixed, this upsert will silently resurrect a previously soft-deleted membership (update: { deletedAt: null }) with no change to createdAt and no audit trail that access was ever removed and re-granted. Worth setting a fresh timestamp (or a dedicated restoredAt field) on the update branch so a re-grant is visible later, independent of whatever authorization ends up wrapping this function.

where: { userId_projectId: { userId, projectId } },
update: {deletedAt: null},
create: { userId, projectId } });
}

async function getUserProject(userId: string, projectId: string) {
return await prisma.userProject.findUnique({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also move inline, anything this short can be inline

where: { userId_projectId: { userId, projectId, deletedAt: null } } }
);
}


async function getUserProjects({userId, projectId}: { userId?: string, projectId?: string }) {
return await prisma.userProject.findMany({
where: { userId, projectId, deletedAt: null },
});
}

async function removeUserFromProject(userId: string, projectId: string) {
return await prisma.userProject.update({
where: { userId_projectId: { userId, projectId, deletedAt: null } },
data: { deletedAt: new Date() }
});
}