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