suggestion mutation backend

This commit is contained in:
Kalle (Sendou) 2021-03-08 22:04:09 +02:00
parent acc20ad5e1
commit 710520d0f6
8 changed files with 46 additions and 97 deletions

View File

@ -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 });
},
});

View File

@ -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([

View File

@ -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;

View File

@ -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,

View File

@ -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"),

View File

@ -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;
};

View File

@ -1,6 +0,0 @@
export class UserError extends Error {
constructor(message: string) {
super(message);
this.name = "UserError";
}
}

View File

@ -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",