From 461a3749dfd39d66849bad969ba1bf67ec96a1ca Mon Sep 17 00:00:00 2001 From: pheralb Date: Thu, 24 Nov 2022 10:57:26 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9C=A8=20Upgrade=20tRPC=20v10=20+=20ad?= =?UTF-8?q?d=20@tanstack/react-query=20&=20prettier-plugin-tailwindcss.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index aea569e..aea4b98 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "slug", "author": "@pheralb_", "description": "A URL shortener built with T3 Stack", - "version": "2.1.0", + "version": "2.1.2", "private": true, "scripts": { "build": "next build", @@ -15,23 +15,23 @@ "@headlessui/react": "1.7.4", "@next-auth/prisma-adapter": "1.0.5", "@prisma/client": "4.6.1", - "@trpc/client": "9.27.4", - "@trpc/next": "9.27.4", - "@trpc/react": "9.27.4", - "@trpc/server": "9.27.4", + "@tanstack/react-query": "4.16.1", + "@trpc/client": "10.1.0", + "@trpc/next": "10.1.0", + "@trpc/server": "10.1.0", + "@trpc/react-query": "10.1.0", "@uiball/loaders": "1.2.6", - "framer-motion": "7.6.7", + "framer-motion": "7.6.10", "nanoid": "4.0.0", - "next": "13.0.4", - "next-auth": "4.16.4", + "next": "13.0.5", + "next-auth": "4.17.0", "next-seo": "5.14.1", "nextjs-progressbar": "0.0.16", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.39.4", + "react-hook-form": "7.39.5", "react-hot-toast": "2.4.0", "react-icons": "4.6.0", - "react-query": "3.39.2", "superjson": "1.11.0", "superkey": "0.1.0-rc.9", "zod": "3.19.1" @@ -40,17 +40,19 @@ "@types/node": "18.11.9", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", - "@typescript-eslint/eslint-plugin": "5.43.0", - "@typescript-eslint/parser": "5.43.0", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", "autoprefixer": "10.4.13", - "eslint": "8.27.0", - "eslint-config-next": "13.0.4", + "eslint": "8.28.0", + "eslint-config-next": "13.0.5", "postcss": "8.4.19", + "prettier": "2.8.0", + "prettier-plugin-tailwindcss": "0.1.13", "prisma": "4.6.1", "tailwindcss": "3.2.4", "typescript": "4.9.3" }, "ct3aMetadata": { - "initVersion": "5.12.0" + "initVersion": "6.10.2" } } From 4ac3a6a6d979b2d836adcce7ddbba23aacbe0bdc Mon Sep 17 00:00:00 2001 From: pheralb Date: Thu, 24 Nov 2022 10:57:41 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Add=20prettier.confi?= =?UTF-8?q?g=20file.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prettier.config.cjs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 prettier.config.cjs diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..96e2ee1 --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,4 @@ +/** @type {import("prettier").Config} */ +module.exports = { + plugins: [require.resolve("prettier-plugin-tailwindcss")], + }; \ No newline at end of file From ab2f9b4de9d90902fdd4aea9df3c9fcdf935b79f Mon Sep 17 00:00:00 2001 From: pheralb Date: Thu, 24 Nov 2022 10:57:59 +0000 Subject: [PATCH 03/13] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Update=20schema=20co?= =?UTF-8?q?nfig.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/env/schema.mjs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/env/schema.mjs b/src/env/schema.mjs index 3bc33af..1083a7f 100644 --- a/src/env/schema.mjs +++ b/src/env/schema.mjs @@ -10,8 +10,17 @@ export const serverSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]), GITHUB_ID: z.string(), GITHUB_CLIENT_SECRET: z.string(), - NEXTAUTH_SECRET: z.string(), - NEXTAUTH_URL: z.string().url(), + NEXTAUTH_SECRET: + process.env.NODE_ENV === "production" + ? z.string().min(1) + : z.string().min(1).optional(), + NEXTAUTH_URL: z.preprocess( + // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL + // Since NextAuth automatically uses the VERCEL_URL if present. + (str) => process.env.VERCEL_URL ?? str, + // VERCEL_URL doesnt include `https` so it cant be validated as a URL + process.env.VERCEL ? z.string() : z.string().url() + ), }); /** From 52767ae0d5ffbf99b93aace61b42da90ab744854 Mon Sep 17 00:00:00 2001 From: pheralb Date: Thu, 24 Nov 2022 14:19:46 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Migrate=20from=20tRP?= =?UTF-8?q?C=20v9=20to=20v10.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/_app.tsx | 33 +------- src/pages/api/trpc/[trpc].ts | 3 +- src/server/router/auth.ts | 10 +++ src/server/router/context.ts | 4 +- src/server/router/index.ts | 12 +-- src/server/router/links.router.ts | 132 +++++++++++++++--------------- src/server/router/trpc.ts | 39 +++++++++ src/utils/trpc.ts | 60 +++++++++----- 8 files changed, 163 insertions(+), 130 deletions(-) create mode 100644 src/server/router/auth.ts create mode 100644 src/server/router/trpc.ts diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 46a5394..5dbdfe4 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,10 +2,7 @@ import type { AppType } from "next/dist/shared/lib/utils"; import type { Session } from "next-auth"; // tRPC => -import type { AppRouter } from "@/server/router"; -import { withTRPC } from "@trpc/next"; -import { httpBatchLink } from "@trpc/client/links/httpBatchLink"; -import { loggerLink } from "@trpc/client/links/loggerLink"; +import { trpc } from "@/utils/trpc"; // Auth => import { SessionProvider } from "next-auth/react"; @@ -26,9 +23,6 @@ import nextSeoConfig from "next-seo.config"; // Next progress => import NextNProgress from "nextjs-progressbar"; -// Superjson => -import superjson from "superjson"; - const MyApp: AppType<{ session: Session | null }> = ({ Component, pageProps: { session, ...pageProps }, @@ -56,27 +50,4 @@ const MyApp: AppType<{ session: Session | null }> = ({ ); }; -const getBaseUrl = () => { - if (typeof window !== "undefined") return ""; - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; - return `http://localhost:${process.env.PORT ?? 3000}`; -}; - -export default withTRPC({ - config({ ctx }) { - const url = `${getBaseUrl()}/api/trpc`; - return { - links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), - httpBatchLink({ url }), - ], - url, - transformer: superjson, - }; - }, - ssr: false, -})(MyApp); +export default trpc.withTRPC(MyApp); diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index 0a2c8d5..43fe7be 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -1,4 +1,5 @@ import { createNextApiHandler } from "@trpc/server/adapters/next"; + import { env } from "@/env/server.mjs"; import { appRouter } from "@/server/router"; import { createContext } from "@/server/router/context"; @@ -9,7 +10,7 @@ export default createNextApiHandler({ onError: env.NODE_ENV === "development" ? ({ path, error }) => { - console.error(`[api/trpc/] ❌ tRPC failed on ${path}: ${error}`); + console.error(`❌ tRPC failed on ${path}: ${error}`); } : undefined, }); diff --git a/src/server/router/auth.ts b/src/server/router/auth.ts new file mode 100644 index 0000000..eed80f3 --- /dev/null +++ b/src/server/router/auth.ts @@ -0,0 +1,10 @@ +import { router, publicProcedure, protectedProcedure } from "./trpc"; + +export const authRouter = router({ + getSession: publicProcedure.query(({ ctx }) => { + return ctx.session; + }), + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/src/server/router/context.ts b/src/server/router/context.ts index 7359fa4..f43f16a 100644 --- a/src/server/router/context.ts +++ b/src/server/router/context.ts @@ -16,7 +16,7 @@ export const createContextInner = async (opts: CreateContextOptions) => { }; export const createContext = async ( - opts: trpcNext.CreateNextContextOptions, + opts: trpcNext.CreateNextContextOptions ) => { const { req, res } = opts; const session = await getServerAuthSession({ req, res }); @@ -25,7 +25,7 @@ export const createContext = async ( }); }; -type Context = trpc.inferAsyncReturnType; +export type Context = trpc.inferAsyncReturnType; export const createRouter = () => trpc.router(); diff --git a/src/server/router/index.ts b/src/server/router/index.ts index 23856f1..de69832 100644 --- a/src/server/router/index.ts +++ b/src/server/router/index.ts @@ -1,11 +1,11 @@ -import { createRouter } from "./context"; -import superjson from "superjson"; - +import { router } from "./trpc"; +import { authRouter } from "./auth"; import { linkRouter } from "./links.router"; -export const appRouter = createRouter() - .transformer(superjson) - .merge("links.", linkRouter); +export const appRouter = router({ + links: linkRouter, + auth: authRouter, +}); // export type definition of API export type AppRouter = typeof appRouter; diff --git a/src/server/router/links.router.ts b/src/server/router/links.router.ts index c5a7dbe..21f1794 100644 --- a/src/server/router/links.router.ts +++ b/src/server/router/links.router.ts @@ -4,89 +4,85 @@ import { getSingleLinkSchema, EditLinkSchema, } from "@/schema/link.schema"; -import { createProtectedRouter } from "./context"; -import { prisma } from "@/server/db/client"; -import { TRPCError } from "@trpc/server"; -export const linkRouter = createProtectedRouter() - // Create new link => - .mutation("create-link", { - input: CreateLinkSchema, - async resolve({ ctx, input }) { - const existedSlug = await prisma.link.findUnique({ - where: { slug: input.slug }, - }); +import { z } from "zod"; +import { router, publicProcedure } from "./trpc"; - if (existedSlug) - throw new TRPCError({ - code: "CONFLICT", - message: - "Custom slug not available. Type another one or click on random.", - }); - - const newLink = await prisma.link?.create({ +export const linkRouter = router({ + // Create new link => + createLink: publicProcedure + .input(CreateLinkSchema) + .mutation(({ ctx, input }) => { + const newLink = ctx.prisma.link.create({ data: { ...input, - creatorId: ctx.session.user.id, + creatorId: ctx.session?.user?.id, }, }); return newLink; - }, - }) + }), + // Edit link => - .mutation("edit-link", { - input: EditLinkSchema, - async resolve({ ctx, input }) { - const editedLink = await prisma.link.update({ - where: { slug: input.slug }, - data: { - ...input, - creatorId: ctx.session.user.id, - }, - }); - return editedLink; - }, - }) + editLink: publicProcedure.input(EditLinkSchema).mutation(({ ctx, input }) => { + const editedLink = ctx.prisma.link.update({ + where: { slug: input.slug }, + data: { + ...input, + creatorId: ctx.session?.user?.id, + }, + }); + return editedLink; + }), + // Delete link => - .mutation("delete-link", { - input: getSingleLinkSchema, - async resolve({ ctx, input }) { - const deletedLink = await prisma.link.delete({ + deleteLink: publicProcedure + .input(getSingleLinkSchema) + .mutation(({ ctx, input }) => { + const deletedLink = ctx.prisma.link.delete({ where: { id: input.linkId }, }); return deletedLink; - }, - }) - // Fetch links => - .query("links", { - input: FilterLinkSchema, - async resolve({ ctx, input }) { - return prisma.link?.findMany({ + }), + + // Get all links => + allLinks: publicProcedure.input(FilterLinkSchema).query(({ ctx, input }) => { + return ctx.prisma.link?.findMany({ + where: { + creatorId: ctx.session?.user?.id, + AND: input.filter + ? [ + { + OR: [ + { url: { contains: input.filter } }, + { slug: { contains: input.filter } }, + { description: { contains: input.filter } }, + ], + }, + ] + : undefined, + }, + }); + }), + + // Get single link => + singleLink: publicProcedure + .input(getSingleLinkSchema) + .query(({ ctx, input }) => { + return ctx.prisma.link?.findUnique({ where: { - creatorId: ctx.session.user.id, - AND: input.filter - ? [ - { - OR: [ - { url: { contains: input.filter } }, - { slug: { contains: input.filter } }, - { description: { contains: input.filter } }, - ], - }, - ] - : undefined, + id: input.linkId, }, }); - }, - }) - // Get single link info => - .query("single-link", { - input: getSingleLinkSchema, - resolve({ input, ctx }) { - return prisma.link?.findUnique({ + }), + + // Check if slug is available => + checkSlug: publicProcedure + .input(z.object({ customSlug: z.string().nullish() }).nullish()) + .query(({ ctx, input }) => { + return ctx.prisma.link?.findUnique({ where: { - id: input.linkId, + slug: input?.customSlug, }, }); - }, - }); + }), +}); diff --git a/src/server/router/trpc.ts b/src/server/router/trpc.ts new file mode 100644 index 0000000..bc341f9 --- /dev/null +++ b/src/server/router/trpc.ts @@ -0,0 +1,39 @@ +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; + +import { type Context } from "./context"; + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape }) { + return shape; + }, +}); + +export const router = t.router; + +/** + * Unprotected procedure + **/ +export const publicProcedure = t.procedure; + +/** + * Reusable middleware to ensure + * users are logged in + */ +const isAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected procedure + **/ +export const protectedProcedure = t.procedure.use(isAuthed); \ No newline at end of file diff --git a/src/utils/trpc.ts b/src/utils/trpc.ts index eb7f9bb..0f59d26 100644 --- a/src/utils/trpc.ts +++ b/src/utils/trpc.ts @@ -1,26 +1,42 @@ -// src/utils/trpc.ts -import type { AppRouter } from "@/server/router"; -import { createReactQueryHooks } from "@trpc/react"; -import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server"; +import superjson from "superjson"; +import { httpBatchLink, loggerLink } from "@trpc/client"; +import { createTRPCNext } from "@trpc/next"; +import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; -export const trpc = createReactQueryHooks(); +import { type AppRouter } from "@/server/router"; -/** - * These are helper types to infer the input and output of query resolvers - * @example type HelloOutput = inferQueryOutput<'hello'> - */ -export type inferQueryOutput< - TRouteKey extends keyof AppRouter["_def"]["queries"], -> = inferProcedureOutput; - -export type inferQueryInput< - TRouteKey extends keyof AppRouter["_def"]["queries"], -> = inferProcedureInput; +const getBaseUrl = () => { + if (typeof window !== "undefined") return ""; // browser should use relative url + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url + return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost +}; -export type inferMutationOutput< - TRouteKey extends keyof AppRouter["_def"]["mutations"], -> = inferProcedureOutput; +export const trpc = createTRPCNext({ + config() { + return { + transformer: superjson, + links: [ + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + }), + httpBatchLink({ + url: `${getBaseUrl()}/api/trpc`, + }), + ], + }; + }, + ssr: false, +}); -export type inferMutationInput< - TRouteKey extends keyof AppRouter["_def"]["mutations"], -> = inferProcedureInput; +/** + * Inference helper for inputs + * @example type HelloInput = RouterInputs['example']['hello'] + **/ +export type RouterInputs = inferRouterInputs; +/** + * Inference helper for outputs + * @example type HelloOutput = RouterOutputs['example']['hello'] + **/ +export type RouterOutputs = inferRouterOutputs; From fc4d09e6a97a89e95b55cd1e48190160301170ee Mon Sep 17 00:00:00 2001 From: pheralb Date: Thu, 24 Nov 2022 14:20:10 +0000 Subject: [PATCH 05/13] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Update=20functions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/functions/create/index.tsx | 38 ++++++++++++----------- src/components/functions/delete/index.tsx | 6 ++-- src/components/functions/edit/index.tsx | 8 ++--- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/components/functions/create/index.tsx b/src/components/functions/create/index.tsx index 426e4c9..77fedae 100644 --- a/src/components/functions/create/index.tsx +++ b/src/components/functions/create/index.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { trpc } from "@/utils/trpc"; import { CreateLinkInput } from "@/schema/link.schema"; -import { BiRocket } from "react-icons/bi"; +import { BiRefresh, BiRocket } from "react-icons/bi"; import { nanoid } from "nanoid"; import toast from "react-hot-toast"; @@ -21,7 +21,7 @@ const Create = () => { const [loading, setLoading] = useState(false); const router = useRouter(); - const { mutate, error } = trpc.useMutation(["links.create-link"], { + const { mutate, error } = trpc.links.createLink.useMutation({ onSuccess: () => { router.push(`/dash`); setLoading(false); @@ -34,17 +34,23 @@ const Create = () => { }, }); }, - onError: () => { + onError: ({ message }) => { setLoading(false); + setError("slug", { + type: "manual", + message: "Slug already exists. Please try another one or click 'Randomize' button.", + }); }, }); const onSubmit = (values: CreateLinkInput) => { - const areEquals = values.url === values.slug; - if (areEquals) { - return setError("slug", { - message: "The original URL and the custom URL cannot be the same", + // Check if slug & url are equals to prevent infinite redirect => + if (values.slug === values.url) { + setError("url", { + type: "manual", + message: "The URL and the slug cannot be the same", }); + return; } setLoading(true); mutate(values); @@ -58,18 +64,13 @@ const Create = () => { return (
- {error && ( - -

{error.message}

-
- )}
{ pattern: { value: /^https?:\/\//i, message: - "Please enter a valid URL. It should start with http:// or https://", + "Please enter a valid URL. It should start with https://.", }, })} /> @@ -92,12 +93,12 @@ const Create = () => {

https://slug.vercel.app/s/

-
+
{
{errors.slug && {errors.slug.message}} @@ -123,7 +125,7 @@ const Create = () => {