Skip to content

Commit

Permalink
add referential actions to the database (#91)
Browse files Browse the repository at this point in the history
* add referential actions to the database

* formatting
  • Loading branch information
cmintey authored Jan 1, 2024
1 parent 8dce023 commit 96591aa
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_user_group_membership" (
"id" TEXT NOT NULL PRIMARY KEY,
"active" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"roleId" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "user_group_membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_group_membership_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_group_membership_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_user_group_membership" ("active", "groupId", "id", "roleId", "userId") SELECT "active", "groupId", "id", "roleId", "userId" FROM "user_group_membership";
DROP TABLE "user_group_membership";
ALTER TABLE "new_user_group_membership" RENAME TO "user_group_membership";
CREATE UNIQUE INDEX "user_group_membership_id_key" ON "user_group_membership"("id");
CREATE TABLE "new_items" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"price" TEXT,
"url" TEXT,
"note" TEXT,
"image_url" TEXT,
"userId" TEXT NOT NULL,
"addedById" TEXT NOT NULL,
"pledgedById" TEXT,
"approved" BOOLEAN NOT NULL DEFAULT true,
"purchased" BOOLEAN NOT NULL DEFAULT false,
"groupId" TEXT,
CONSTRAINT "items_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "items_addedById_fkey" FOREIGN KEY ("addedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "items_pledgedById_fkey" FOREIGN KEY ("pledgedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "items_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_items" ("addedById", "approved", "groupId", "id", "image_url", "name", "note", "pledgedById", "price", "purchased", "url", "userId") SELECT "addedById", "approved", "groupId", "id", "image_url", "name", "note", "pledgedById", "price", "purchased", "url", "userId" FROM "items";
DROP TABLE "items";
ALTER TABLE "new_items" RENAME TO "items";
CREATE UNIQUE INDEX "items_id_key" ON "items"("id");
CREATE INDEX "items_userId_idx" ON "items"("userId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
12 changes: 6 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ model Group {
model UserGroupMembership {
id String @id @unique @default(uuid())
active Boolean @default(false)
user User @relation(fields: [userId], references: [id])
group Group @relation(fields: [groupId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id])
userId String
groupId String
Expand All @@ -83,15 +83,15 @@ model Item {
url String?
note String?
image_url String?
user User @relation(name: "MyItems", fields: [userId], references: [id])
user User @relation(name: "MyItems", fields: [userId], references: [id], onDelete: Cascade)
userId String
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id])
addedBy User @relation(name: "AddedItems", fields: [addedById], references: [id], onDelete: Cascade)
addedById String
pledgedBy User? @relation(name: "PledgedItems", fields: [pledgedById], references: [id])
pledgedBy User? @relation(name: "PledgedItems", fields: [pledgedById], references: [id], onDelete: SetNull)
pledgedById String?
approved Boolean @default(true)
purchased Boolean @default(false)
group Group? @relation(fields: [groupId], references: [id])
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String?
@@index([userId])
Expand Down
17 changes: 17 additions & 0 deletions src/lib/server/image-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sharp from "sharp";
import { unlink } from "fs/promises";

export const createImage = async (username: string, image: File): Promise<string | null> => {
let filename = null;
Expand All @@ -13,3 +14,19 @@ export const createImage = async (username: string, image: File): Promise<string

return filename;
};

export const deleteImage = async (filename: string): Promise<void> => {
try {
await unlink(`uploads/${filename}`);
} catch (e) {
console.warn("Unable to delete file: ", filename);
}
};

export const tryDeleteImage = async (imageUrl: string): Promise<void> => {
try {
new URL(imageUrl);
} catch {
await deleteImage(imageUrl);
}
};
46 changes: 27 additions & 19 deletions src/lib/server/invite-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,26 @@ import generateToken, { hashToken } from "./token";
export const inviteUser = async ({ url, request }: RequestEvent) => {
const token = await generateToken();
const tokenUrl = new URL(`/signup?token=${token}`, url);
const formData = Object.fromEntries(await request.formData());
let schema;

const config = await getConfig();

if (!config.smtp.enable) {
await client.signupToken.create({
data: {
hashedToken: hashToken(token)
}
if (config.smtp.enable) {
schema = z.object({
"invite-email": z.string().email(),
"invite-group": z.string().optional()
});
} else {
schema = z.object({
"invite-email": z.string().optional(),
"invite-group": z.string().optional()
});

return { action: "invite-email", success: true, url: tokenUrl.href };
}

const formData = Object.fromEntries(await request.formData());
const schema = z.object({
"invite-email": z.string().email(),
"invite-group": z.string().min(1)
});
const data = schema.safeParse(formData);

const emailData = schema.safeParse(formData);

if (!emailData.success) {
const errors = emailData.error.errors.map((error) => {
if (!data.success) {
const errors = data.error.errors.map((error) => {
return {
field: error.path[0],
message: error.message
Expand All @@ -39,13 +36,24 @@ export const inviteUser = async ({ url, request }: RequestEvent) => {
return fail(400, { action: "invite-email", error: true, errors });
}

if (!config.smtp.enable) {
await client.signupToken.create({
data: {
hashedToken: hashToken(token),
groupId: data.data["invite-group"]
}
});

return { action: "invite-email", success: true, url: tokenUrl.href };
}

await client.signupToken.create({
data: {
hashedToken: hashToken(token),
groupId: emailData.data["invite-group"]
groupId: data.data["invite-group"]
}
});

await sendSignupLink(emailData.data["invite-email"], tokenUrl.href);
await sendSignupLink(data.data["invite-email"]!, tokenUrl.href);
return { action: "invite-email", success: true, url: null };
};
13 changes: 12 additions & 1 deletion src/routes/account/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fail, redirect } from "@sveltejs/kit";
import { z } from "zod";
import type { Actions, PageServerLoad } from "./$types";
import type { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { createImage } from "$lib/server/image-util";
import { createImage, tryDeleteImage } from "$lib/server/image-util";

export const load: PageServerLoad = async ({ locals }) => {
const session = await locals.validate();
Expand Down Expand Up @@ -73,6 +73,14 @@ export const actions: Actions = {
const filename = await createImage(session.user.username, image);

if (filename) {
const user = await client.user.findUniqueOrThrow({
select: {
picture: true
},
where: {
id: session.user.userId
}
});
await client.user.update({
where: {
id: session.user.userId
Expand All @@ -81,6 +89,9 @@ export const actions: Actions = {
picture: filename
}
});
if (user.picture) {
await tryDeleteImage(user.picture);
}
}
},

Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin/users/[username]/+page@.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
invalidateAll();
toastStore.trigger({
message: `${userId} was deleted`,
message: `${username} was deleted`,
autohide: true,
timeout: 5000
});
Expand Down
24 changes: 22 additions & 2 deletions src/routes/api/items/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { client } from "$lib/server/prisma";
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { _authCheck } from "../groups/[groupId]/auth";
import { tryDeleteImage } from "$lib/server/image-util";

export const DELETE: RequestHandler = async ({ locals, request }) => {
const groupId = new URL(request.url).searchParams.get("groupId");
Expand All @@ -23,13 +24,32 @@ export const DELETE: RequestHandler = async ({ locals, request }) => {
}

try {
const items = await client.item.deleteMany({
const items = await client.item.findMany({
select: {
id: true,
image_url: true
},
where: {
groupId: groupId ? groupId : undefined,
pledgedById: claimed && Boolean(claimed) ? { not: null } : undefined
}
});
return new Response(JSON.stringify(items), { status: 200 });

for (const item of items) {
if (item.image_url) {
await tryDeleteImage(item.image_url);
}
}

const deletedItems = await client.item.deleteMany({
where: {
id: {
in: items.map((item) => item.id)
}
}
});

return new Response(JSON.stringify(deletedItems), { status: 200 });
} catch (e) {
error(500, "Unable to delete items");
}
Expand Down
27 changes: 21 additions & 6 deletions src/routes/api/items/[itemId]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getConfig } from "$lib/server/config";
import { getActiveMembership } from "$lib/server/group-membership";
import { tryDeleteImage } from "$lib/server/image-util";
import { client } from "$lib/server/prisma";
import { error, type RequestHandler } from "@sveltejs/kit";
import assert from "assert";
Expand Down Expand Up @@ -28,7 +29,8 @@ const validateItem = async (itemId: string | undefined, session: Session | null)
select: {
username: true
}
}
},
image_url: true
}
});

Expand Down Expand Up @@ -68,10 +70,15 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
select: {
username: true
}
}
},
image_url: true
}
});

if (item.image_url) {
await tryDeleteImage(item.image_url);
}

return new Response(JSON.stringify(item), { status: 200 });
} catch (e) {
error(404, "item id not found");
Expand All @@ -81,7 +88,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
export const PATCH: RequestHandler = async ({ params, locals, request }) => {
const session = await locals.validate();

await validateItem(params?.itemId, session);
const item = await validateItem(params?.itemId, session);

const body = (await request.json()) as Record<string, unknown>;
const data: {
Expand All @@ -97,12 +104,16 @@ export const PATCH: RequestHandler = async ({ params, locals, request }) => {
approved?: boolean;
purchased?: boolean;
} = {};
let deleteOldImage = false;

if (body.name && typeof body.name === "string") data.name = body.name;
if (body.price && typeof body.price === "string") data.price = body.price;
if (body.url && typeof body.url === "string") data.url = body.url;
if (body.note && typeof body.note === "string") data.note = body.note;
if (body.image_url && typeof body.image_url === "string") data.image_url = body.image_url;
if (body.image_url && typeof body.image_url === "string") {
data.image_url = body.image_url;
deleteOldImage = true;
}
if (body.pledgedById && typeof body.pledgedById === "string") {
if (body.pledgedById === "0") {
data.pledgedBy = {
Expand All @@ -120,15 +131,19 @@ export const PATCH: RequestHandler = async ({ params, locals, request }) => {
if (Object.keys(body).includes("purchased") && typeof body.purchased === "boolean") data.purchased = body.purchased;

try {
const item = await client.item.update({
const updatedItem = await client.item.update({
where: {
// @ts-expect-error params.itemId is checked in a previous function
id: parseInt(params.itemId)
},
data
});

return new Response(JSON.stringify(item), { status: 200 });
if (deleteOldImage && item.image_url) {
await tryDeleteImage(item.image_url);
}

return new Response(JSON.stringify(updatedItem), { status: 200 });
} catch (e) {
error(404, "item id not found");
}
Expand Down
9 changes: 4 additions & 5 deletions src/routes/api/users/[userId]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Role } from "$lib/schema";
import { tryDeleteImage } from "$lib/server/image-util";
import { client } from "$lib/server/prisma";
import { type RequestHandler, error } from "@sveltejs/kit";

Expand Down Expand Up @@ -30,16 +31,14 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
}

try {
await client.userGroupMembership.deleteMany({
where: {
userId: user.id
}
});
const deletedUser = await client.user.delete({
where: {
id: user.id
}
});
if (deletedUser && deletedUser.picture) {
await tryDeleteImage(deletedUser.picture);
}

return new Response(JSON.stringify(deletedUser), { status: 200 });
} catch (e) {
Expand Down
12 changes: 11 additions & 1 deletion src/routes/wishlists/[username]/edit/[itemId]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { client } from "$lib/server/prisma";
import { getConfig } from "$lib/server/config";
import { getActiveMembership } from "$lib/server/group-membership";
import { createImage } from "$lib/server/image-util";
import { createImage, tryDeleteImage } from "$lib/server/image-util";

export const load: PageServerLoad = async ({ locals, params }) => {
const session = await locals.validate();
Expand Down Expand Up @@ -74,6 +74,12 @@ export const actions: Actions = {

const filename = await createImage(session.user.username, image);

const item = await client.item.findUniqueOrThrow({
where: {
id: parseInt(params.itemId)
}
});

await client.item.update({
where: {
id: parseInt(params.itemId)
Expand All @@ -87,6 +93,10 @@ export const actions: Actions = {
}
});

if (filename && item.image_url && item.image_url !== filename) {
await tryDeleteImage(item.image_url);
}

const ref = new URL(request.url).searchParams.get("ref");
redirect(302, ref ?? `/wishlists/${params.username}`);
}
Expand Down

0 comments on commit 96591aa

Please sign in to comment.