mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-25 07:32:19 -05:00
suggestion mutation backend
This commit is contained in:
parent
acc20ad5e1
commit
710520d0f6
|
|
@ -1,4 +1,5 @@
|
|||
import { createRouter } from "pages/api/trpc/[trpc]";
|
||||
import { throwIfNotLoggedIn } from "utils/api";
|
||||
import { suggestionFullSchema } from "utils/validators/suggestion";
|
||||
import service from "./service";
|
||||
|
||||
|
|
@ -15,8 +16,9 @@ const plusApi = createRouter()
|
|||
})
|
||||
.mutation("suggestion", {
|
||||
input: suggestionFullSchema,
|
||||
resolve({ input }) {
|
||||
return service.getPlusStatuses();
|
||||
resolve({ input, ctx }) {
|
||||
const user = throwIfNotLoggedIn(ctx.user);
|
||||
return service.addSuggestion({ input, userId: user.id });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { httpError } from "@trpc/server";
|
||||
import prisma from "prisma/client";
|
||||
import { UserError } from "utils/errors";
|
||||
import { getPercentageFromCounts, getVotingRange } from "utils/plus";
|
||||
import { userBasicSelection } from "utils/prisma";
|
||||
import { suggestionFullSchema } from "utils/validators/suggestion";
|
||||
import { vouchSchema } from "utils/validators/vouch";
|
||||
import * as z from "zod";
|
||||
|
||||
export type PlusStatuses = Prisma.PromiseReturnType<typeof getPlusStatuses>;
|
||||
|
||||
|
|
@ -163,23 +164,19 @@ const getDistinctSummaryMonths = () => {
|
|||
};
|
||||
|
||||
const addSuggestion = async ({
|
||||
data,
|
||||
input,
|
||||
userId,
|
||||
}: {
|
||||
data: unknown;
|
||||
input: z.infer<typeof suggestionFullSchema>;
|
||||
userId: number;
|
||||
}) => {
|
||||
const parsedData = {
|
||||
...suggestionFullSchema.parse(data),
|
||||
suggesterId: userId,
|
||||
};
|
||||
const [suggestions, plusStatuses] = await Promise.all([
|
||||
prisma.plusSuggestion.findMany({}),
|
||||
prisma.plusStatus.findMany({}),
|
||||
]);
|
||||
const existingSuggestion = suggestions.find(
|
||||
({ tier, suggestedId }) =>
|
||||
tier === parsedData.tier && suggestedId === parsedData.suggestedId
|
||||
tier === input.tier && suggestedId === input.suggestedId
|
||||
);
|
||||
|
||||
// every user can only send one new suggestion per month
|
||||
|
|
@ -189,7 +186,7 @@ const addSuggestion = async ({
|
|||
isResuggestion === false && suggesterId === userId
|
||||
);
|
||||
if (usersSuggestion) {
|
||||
throw new UserError("already made a new suggestion");
|
||||
httpError.badRequest("already made a new suggestion");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,40 +197,44 @@ const addSuggestion = async ({
|
|||
if (
|
||||
!suggesterPlusStatus ||
|
||||
!suggesterPlusStatus.membershipTier ||
|
||||
suggesterPlusStatus.membershipTier > parsedData.tier
|
||||
suggesterPlusStatus.membershipTier > input.tier
|
||||
) {
|
||||
throw new UserError(
|
||||
httpError.badRequest(
|
||||
"not a member of high enough tier to suggest for this tier"
|
||||
);
|
||||
}
|
||||
|
||||
if (suggestedUserAlreadyHasAccess()) {
|
||||
throw new UserError("suggested user already has access");
|
||||
throw httpError.badRequest("suggested user already has access");
|
||||
}
|
||||
|
||||
if (getVotingRange().isHappening) {
|
||||
throw new UserError("voting has already started");
|
||||
httpError.badRequest("voting has already started");
|
||||
}
|
||||
|
||||
return prisma.$transaction([
|
||||
prisma.plusSuggestion.create({
|
||||
data: { ...parsedData, isResuggestion: !!existingSuggestion },
|
||||
data: {
|
||||
...input,
|
||||
suggesterId: userId,
|
||||
isResuggestion: !!existingSuggestion,
|
||||
},
|
||||
}),
|
||||
prisma.plusStatus.upsert({
|
||||
where: { userId: parsedData.suggestedId },
|
||||
create: { region: parsedData.region, userId: parsedData.suggestedId },
|
||||
where: { userId: input.suggestedId },
|
||||
create: { region: input.region, userId: input.suggestedId },
|
||||
update: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
function suggestedUserAlreadyHasAccess() {
|
||||
const suggestedPlusStatus = plusStatuses.find(
|
||||
(status) => status.userId === parsedData.suggestedId
|
||||
(status) => status.userId === input.suggestedId
|
||||
);
|
||||
return Boolean(
|
||||
suggestedPlusStatus &&
|
||||
((suggestedPlusStatus.membershipTier ?? 999) <= parsedData.tier ||
|
||||
(suggestedPlusStatus.vouchTier ?? 999) <= parsedData.tier)
|
||||
((suggestedPlusStatus.membershipTier ?? 999) <= input.tier ||
|
||||
(suggestedPlusStatus.vouchTier ?? 999) <= input.tier)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -253,17 +254,17 @@ const addVouch = async ({
|
|||
);
|
||||
|
||||
if ((suggesterPlusStatus?.canVouchFor ?? Infinity) > parsedData.tier) {
|
||||
throw new UserError(
|
||||
httpError.badRequest(
|
||||
"not a member of high enough tier to vouch for this tier"
|
||||
);
|
||||
}
|
||||
|
||||
if (vouchedUserAlreadyHasAccess()) {
|
||||
throw new UserError("vouched user already has access");
|
||||
httpError.badRequest("vouched user already has access");
|
||||
}
|
||||
|
||||
if (getVotingRange().isHappening) {
|
||||
throw new UserError("voting has already started");
|
||||
httpError.badRequest("voting has already started");
|
||||
}
|
||||
|
||||
return prisma.$transaction([
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import plusService from "app/plus/service";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getMySession } from "utils/api";
|
||||
import { UserError } from "utils/errors";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
const suggestionsHandler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) => {
|
||||
const user = await getMySession(req);
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
await getHandler(req, res);
|
||||
break;
|
||||
case "POST":
|
||||
await postHandler(req, res);
|
||||
break;
|
||||
default:
|
||||
res.status(405).end();
|
||||
}
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
try {
|
||||
await plusService.addSuggestion({ data: req.body, userId: user.id });
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
res.status(400).json({ message: e.message });
|
||||
} else if (e instanceof UserError) {
|
||||
res.status(400).json({ message: e.message });
|
||||
} else {
|
||||
console.error(e.message);
|
||||
res.status(500).end();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async function getHandler(_req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
res.status(200).json(await plusService.getSuggestions());
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default suggestionsHandler;
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import * as trpc from "@trpc/server";
|
||||
import { inferProcedureOutput } from "@trpc/server";
|
||||
import { inferAsyncReturnType, inferProcedureOutput } from "@trpc/server";
|
||||
import * as trpcNext from "@trpc/server/dist/adapters/next";
|
||||
import plusApi from "app/plus/api";
|
||||
import superjson from "superjson";
|
||||
import { getMySession } from "utils/api";
|
||||
import { trpc as trcpReactQuery } from "utils/trpc";
|
||||
|
||||
export type Context = {};
|
||||
const createContext = async ({
|
||||
req,
|
||||
res,
|
||||
}: trpcNext.CreateNextContextOptions) => {
|
||||
return {};
|
||||
const createContext = async ({ req }: trpcNext.CreateNextContextOptions) => {
|
||||
const user = await getMySession(req);
|
||||
return { user };
|
||||
};
|
||||
|
||||
type Context = inferAsyncReturnType<typeof createContext>;
|
||||
|
||||
export function createRouter() {
|
||||
return trpc.router<Context>();
|
||||
}
|
||||
|
|
@ -30,7 +31,8 @@ export type inferQueryOutput<
|
|||
TRouteKey extends keyof AppRouter["_def"]["queries"]
|
||||
> = inferProcedureOutput<AppRouter["_def"]["queries"][TRouteKey]>;
|
||||
|
||||
// export API handler
|
||||
export const ssr = trcpReactQuery.ssr(appRouter, { user: null });
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import PlusHomePage from "app/plus/components/PlusHomePage";
|
||||
import HeaderBanner from "components/layout/HeaderBanner";
|
||||
import { appRouter } from "pages/api/trpc/[trpc]";
|
||||
import { ssr } from "pages/api/trpc/[trpc]";
|
||||
import { trpc } from "utils/trpc";
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const ssr = trpc.ssr(appRouter, {});
|
||||
|
||||
await Promise.all([
|
||||
ssr.prefetchQuery("plus.suggestions"),
|
||||
ssr.prefetchQuery("plus.statuses"),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { User } from "@prisma/client";
|
||||
import { httpError } from "@trpc/server";
|
||||
import { NextApiRequest } from "next";
|
||||
import { getSession } from "next-auth/client";
|
||||
|
||||
|
|
@ -13,3 +14,9 @@ export const getMySession = (req: NextApiRequest): Promise<User | null> => {
|
|||
// @ts-expect-error
|
||||
return getSession({ req });
|
||||
};
|
||||
|
||||
export const throwIfNotLoggedIn = (user: User | null) => {
|
||||
if (!user) throw httpError.unauthorized();
|
||||
|
||||
return user;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export class UserError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UserError";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { createReactQueryHooks, createTRPCClient } from "@trpc/react";
|
||||
import type { AppRouter } from "pages/api/trpc/[trpc]";
|
||||
import { QueryClient } from "react-query";
|
||||
import superjson from "superjson";
|
||||
import type { AppRouter } from "../pages/api/trpc/[trpc]";
|
||||
|
||||
export const client = createTRPCClient<AppRouter>({
|
||||
url: "/api/trpc",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user