From 2e3d02e9c04bcbb9906b4f9695e4bbe224c12274 Mon Sep 17 00:00:00 2001 From: Julius Marminge <julius0216@outlook.com> Date: Mon, 22 Jul 2024 09:31:44 +0200 Subject: [PATCH] fix: async/await in `getLatest` procedure, reduce dev delay and add fuzz (#1950) --- .../src/server/api/routers/post/base.ts | 26 ++++++---- .../api/routers/post/with-auth-drizzle.ts | 7 +-- .../api/routers/post/with-auth-prisma.ts | 7 +-- .../src/server/api/routers/post/with-auth.ts | 3 -- .../server/api/routers/post/with-drizzle.ts | 7 +-- .../server/api/routers/post/with-prisma.ts | 7 +-- .../extras/src/server/api/trpc-app/base.ts | 25 +++++++++- .../src/server/api/trpc-app/with-auth-db.ts | 47 ++++++++++++++----- .../src/server/api/trpc-app/with-auth.ts | 47 ++++++++++++++----- .../extras/src/server/api/trpc-app/with-db.ts | 25 +++++++++- .../extras/src/server/api/trpc-pages/base.ts | 25 +++++++++- .../src/server/api/trpc-pages/with-auth-db.ts | 47 ++++++++++++++----- .../src/server/api/trpc-pages/with-auth.ts | 47 ++++++++++++++----- .../src/server/api/trpc-pages/with-db.ts | 25 +++++++++- 14 files changed, 265 insertions(+), 80 deletions(-) diff --git a/cli/template/extras/src/server/api/routers/post/base.ts b/cli/template/extras/src/server/api/routers/post/base.ts index 1673517554..afe46d87e2 100644 --- a/cli/template/extras/src/server/api/routers/post/base.ts +++ b/cli/template/extras/src/server/api/routers/post/base.ts @@ -2,10 +2,17 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; -let post = { - id: 1, - name: "Hello World", -}; +// Mocked DB +interface Post { + id: number; + name: string; +} +const posts: Post[] = [ + { + id: 1, + name: "Hello World", + }, +]; export const postRouter = createTRPCRouter({ hello: publicProcedure @@ -19,14 +26,15 @@ export const postRouter = createTRPCRouter({ create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - - post = { id: post.id + 1, name: input.name }; + const post: Post = { + id: posts.length + 1, + name: input.name, + }; + posts.push(post); return post; }), getLatest: publicProcedure.query(() => { - return post; + return posts.at(-1) ?? null; }), }); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts index 885f2fd143..71449aeb53 100644 --- a/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts +++ b/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts @@ -19,17 +19,14 @@ export const postRouter = createTRPCRouter({ create: protectedProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ ctx, input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - await ctx.db.insert(posts).values({ name: input.name, createdById: ctx.session.user.id, }); }), - getLatest: publicProcedure.query(({ ctx }) => { - const post = ctx.db.query.posts.findFirst({ + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.query.posts.findFirst({ orderBy: (posts, { desc }) => [desc(posts.createdAt)], }); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts index 12aa34ba03..b602dd5776 100644 --- a/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts +++ b/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts @@ -18,9 +18,6 @@ export const postRouter = createTRPCRouter({ create: protectedProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ ctx, input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - return ctx.db.post.create({ data: { name: input.name, @@ -29,8 +26,8 @@ export const postRouter = createTRPCRouter({ }); }), - getLatest: protectedProcedure.query(({ ctx }) => { - const post = ctx.db.post.findFirst({ + getLatest: protectedProcedure.query(async ({ ctx }) => { + const post = await ctx.db.post.findFirst({ orderBy: { createdAt: "desc" }, where: { createdBy: { id: ctx.session.user.id } }, }); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth.ts b/cli/template/extras/src/server/api/routers/post/with-auth.ts index a2072d3116..199b617fe6 100644 --- a/cli/template/extras/src/server/api/routers/post/with-auth.ts +++ b/cli/template/extras/src/server/api/routers/post/with-auth.ts @@ -23,9 +23,6 @@ export const postRouter = createTRPCRouter({ create: protectedProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - post = { id: post.id + 1, name: input.name }; return post; }), diff --git a/cli/template/extras/src/server/api/routers/post/with-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-drizzle.ts index 8c8ac78396..4bbf615b31 100644 --- a/cli/template/extras/src/server/api/routers/post/with-drizzle.ts +++ b/cli/template/extras/src/server/api/routers/post/with-drizzle.ts @@ -15,16 +15,13 @@ export const postRouter = createTRPCRouter({ create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ ctx, input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - await ctx.db.insert(posts).values({ name: input.name, }); }), - getLatest: publicProcedure.query(({ ctx }) => { - const post = ctx.db.query.posts.findFirst({ + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.query.posts.findFirst({ orderBy: (posts, { desc }) => [desc(posts.createdAt)], }); diff --git a/cli/template/extras/src/server/api/routers/post/with-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-prisma.ts index 8337e2f1f8..da1c7997da 100644 --- a/cli/template/extras/src/server/api/routers/post/with-prisma.ts +++ b/cli/template/extras/src/server/api/routers/post/with-prisma.ts @@ -14,9 +14,6 @@ export const postRouter = createTRPCRouter({ create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ ctx, input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); - return ctx.db.post.create({ data: { name: input.name, @@ -24,8 +21,8 @@ export const postRouter = createTRPCRouter({ }); }), - getLatest: publicProcedure.query(({ ctx }) => { - const post = ctx.db.post.findFirst({ + getLatest: publicProcedure.query(async ({ ctx }) => { + const post = await ctx.db.post.findFirst({ orderBy: { createdAt: "desc" }, }); diff --git a/cli/template/extras/src/server/api/trpc-app/base.ts b/cli/template/extras/src/server/api/trpc-app/base.ts index abd41371f5..e1f87a56b3 100644 --- a/cli/template/extras/src/server/api/trpc-app/base.ts +++ b/cli/template/extras/src/server/api/trpc-app/base.ts @@ -70,6 +70,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -77,4 +100,4 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts index 486a81673d..6ef19c56d8 100644 --- a/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts +++ b/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts @@ -78,6 +78,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -85,7 +108,7 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); /** * Protected (authenticated) procedure @@ -95,14 +118,16 @@ export const publicProcedure = t.procedure; * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(({ 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 }, - }, +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ 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 }, + }, + }); }); -}); diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth.ts b/cli/template/extras/src/server/api/trpc-app/with-auth.ts index c6f8177fe1..a77672b2c5 100644 --- a/cli/template/extras/src/server/api/trpc-app/with-auth.ts +++ b/cli/template/extras/src/server/api/trpc-app/with-auth.ts @@ -75,6 +75,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -82,7 +105,7 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); /** * Protected (authenticated) procedure @@ -92,14 +115,16 @@ export const publicProcedure = t.procedure; * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(({ 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 }, - }, +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ 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 }, + }, + }); }); -}); diff --git a/cli/template/extras/src/server/api/trpc-app/with-db.ts b/cli/template/extras/src/server/api/trpc-app/with-db.ts index b760382481..1837df432b 100644 --- a/cli/template/extras/src/server/api/trpc-app/with-db.ts +++ b/cli/template/extras/src/server/api/trpc-app/with-db.ts @@ -73,6 +73,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -80,4 +103,4 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/cli/template/extras/src/server/api/trpc-pages/base.ts b/cli/template/extras/src/server/api/trpc-pages/base.ts index a1429e18a8..8460e3750b 100644 --- a/cli/template/extras/src/server/api/trpc-pages/base.ts +++ b/cli/template/extras/src/server/api/trpc-pages/base.ts @@ -89,6 +89,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -96,4 +119,4 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts index 3e75c9ea71..c7043c272f 100644 --- a/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts +++ b/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts @@ -105,6 +105,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -112,7 +135,7 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); /** * Protected (authenticated) procedure @@ -122,14 +145,16 @@ export const publicProcedure = t.procedure; * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(({ 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 }, - }, +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ 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 }, + }, + }); }); -}); diff --git a/cli/template/extras/src/server/api/trpc-pages/with-auth.ts b/cli/template/extras/src/server/api/trpc-pages/with-auth.ts index fc648166b6..323f1d9a6b 100644 --- a/cli/template/extras/src/server/api/trpc-pages/with-auth.ts +++ b/cli/template/extras/src/server/api/trpc-pages/with-auth.ts @@ -103,6 +103,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -110,7 +133,7 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware); /** * Protected (authenticated) procedure @@ -120,14 +143,16 @@ export const publicProcedure = t.procedure; * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(({ 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 }, - }, +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ 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 }, + }, + }); }); -}); diff --git a/cli/template/extras/src/server/api/trpc-pages/with-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-db.ts index 11edbdb051..96a363064d 100644 --- a/cli/template/extras/src/server/api/trpc-pages/with-db.ts +++ b/cli/template/extras/src/server/api/trpc-pages/with-db.ts @@ -92,6 +92,29 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +/** + * Middleware for timing procedure execution and adding an articifial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + /** * Public (unauthenticated) procedure * @@ -99,4 +122,4 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(timingMiddleware);