From 8fc76d7c8fbcd06e9fa206db1a5dbc271883df23 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 14:12:32 +0300 Subject: [PATCH 1/7] feat(templates): give the generated app a first screen that proves the stack The generated home page opened with ASCII art and an "API Status" card, which proves a health endpoint responds and nothing else. It now leads with the project name and its real stack, and its main panel is a task list backed by the database: adding a task writes a row through the ORM and the API layer, and the row survives a reload. With Better Auth the panel sits behind the session guard and every procedure is a protectedProcedure scoped to the signed-in user. The health check stays as a small footer badge. Queries live in a repository inside the db package rather than in the API layer, so each API layer calls the same four functions and no new dependency wiring was needed. There is no seed data; an empty list that invites the first write is the stronger proof. Covers Drizzle on SQLite, Postgres and MySQL with tRPC and TanStack Router. Other stacks keep working and say they have no database-backed task API yet. --- .../template-snapshots.test.ts.snap | 2060 ++++++++++++----- .../api/trpc/server/src/routers/index.ts.hbs | 39 + .../db/drizzle/base/src/schema/index.ts.hbs | 1 + .../db/drizzle/base/src/tasks.ts.hbs | 63 + .../db/drizzle/mysql/src/schema/task.ts.hbs | 27 + .../drizzle/postgres/src/schema/task.ts.hbs | 27 + .../db/drizzle/sqlite/src/schema/task.ts.hbs | 30 + .../tanstack-router/src/routes/index.tsx.hbs | 258 ++- 8 files changed, 1918 insertions(+), 587 deletions(-) create mode 100644 packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs create mode 100644 packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs create mode 100644 packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs create mode 100644 packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs diff --git a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap index ae2bce170..103a8bb0c 100644 --- a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap @@ -47,6 +47,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: tanstack-ro "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -112,6 +114,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: next-self-f "packages/db/src/index.ts", "packages/db/src/schema/auth.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -155,6 +159,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: astro-react "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -198,6 +204,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: nuxt-standa "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -250,6 +258,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: solid-start "packages/db/src/index.ts", "packages/db/src/schema/auth.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -363,6 +373,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: hono-bun-or "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -428,6 +440,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: hono-openap "packages/db/src/index.ts", "packages/db/src/schema/auth.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -492,6 +506,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: hono-apollo "packages/db/src/index.ts", "packages/db/src/schema/auth.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -572,6 +588,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: typescript- "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -633,6 +651,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: xendit-sign "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -773,6 +793,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: better-auth "packages/db/src/index.ts", "packages/db/src/schema/auth.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -879,6 +901,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: self-next-c "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -933,6 +957,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: self-tansta "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1101,6 +1127,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: algolia-sea "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1159,6 +1187,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: opensearch- "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1217,6 +1247,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: nango-integ "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1275,6 +1307,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: medusa-ecom "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1335,6 +1369,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: cms-keystat "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1395,6 +1431,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: i18n-paragl "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1453,6 +1491,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: ai-cli-root "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1511,6 +1551,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: nx-root-too "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1608,6 +1650,8 @@ exports[`Template Snapshots File Structure Snapshots file structure: native-reac "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", + "packages/db/src/schema/task.ts", + "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -1731,7 +1775,7 @@ exports[`Template Snapshots File Structure Snapshots file structure: java-spring exports[`Template Snapshots Key File Content Snapshots key files: tanstack-router-minimal 1`] = ` { - "fileCount": 57, + "fileCount": 59, "files": [ { "content": "[exists]", @@ -2014,52 +2058,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-tanstack-router-minimal

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -2273,6 +2404,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-tanstack-router-minimal/db/tasks"; // @@ -2285,6 +2424,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -2433,6 +2584,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -2441,6 +2593,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -2523,7 +2683,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: next-self-fullstack 1`] = ` { - "fileCount": 66, + "fileCount": 68, "files": [ { "content": "[exists]", @@ -2942,6 +3102,14 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { protectedProcedure, publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-next-self-fullstack/db/tasks"; // @@ -2954,6 +3122,20 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: protectedProcedure.query(({ ctx }) => listTasks(ctx.session.user.id)), + create: protectedProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ ctx, input }) => createTask(ctx.session.user.id, input.title)), + setCompleted: protectedProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ ctx, input }) => + setTaskCompleted(ctx.session.user.id, input.id, input.completed), + ), + remove: protectedProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ ctx, input }) => deleteTask(ctx.session.user.id, input.id)), + }), privateData: protectedProcedure.query(({ ctx }) => { return { message: "This is private", @@ -3285,6 +3467,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -3293,6 +3476,14 @@ export const accountRelations = relations(account, ({ one }) => ({ , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -3376,7 +3567,7 @@ export const accountRelations = relations(account, ({ one }) => ({ exports[`Template Snapshots Key File Content Snapshots key files: astro-react-integration 1`] = ` { - "fileCount": 43, + "fileCount": 45, "files": [ { "content": "[exists]", @@ -3782,6 +3973,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -3790,6 +3982,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -3871,7 +4071,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: nuxt-standalone 1`] = ` { - "fileCount": 43, + "fileCount": 45, "files": [ { "content": "[exists]", @@ -4316,6 +4516,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -4324,6 +4525,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -4407,7 +4616,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: solid-start-v2-fullstack 1`] = ` { - "fileCount": 52, + "fileCount": 54, "files": [ { "content": "[exists]", @@ -5170,6 +5379,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -5178,6 +5388,14 @@ export const accountRelations = relations(account, ({ one }) => ({ , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -5544,52 +5762,46 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-express-node-trpc

+

+ TanStack Router and express, postgres through prisma. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -6022,7 +6234,7 @@ export default prisma; exports[`Template Snapshots Key File Content Snapshots key files: hono-bun-orpc 1`] = ` { - "fileCount": 57, + "fileCount": 59, "files": [ { "content": "[exists]", @@ -6345,52 +6557,46 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { orpc } from "@/utils/orpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery(orpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-hono-bun-orpc

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -6754,6 +6960,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -6762,6 +6969,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -6844,7 +7059,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: hono-openapi 1`] = ` { - "fileCount": 66, + "fileCount": 68, "files": [ { "content": "[exists]", @@ -7202,28 +7417,12 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { env } from "@snapshot-hono-openapi/env/web"; import { useEffect, useState } from "react"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const [healthCheck, setHealthCheck] = useState({ isLoading: true, data: false }); @@ -7242,25 +7441,34 @@ function HomeComponent() { }, []); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-hono-openapi

+

+ TanStack Router and hono, sqlite through drizzle, Better Auth. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -7904,6 +8112,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -7912,6 +8121,14 @@ export const accountRelations = relations(account, ({ one }) => ({ , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -7994,7 +8211,7 @@ export const accountRelations = relations(account, ({ one }) => ({ exports[`Template Snapshots Key File Content Snapshots key files: hono-apollo-server 1`] = ` { - "fileCount": 65, + "fileCount": 67, "files": [ { "content": "[exists]", @@ -8296,28 +8513,12 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { graphqlFetch } from "@/utils/graphql"; import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery({ @@ -8326,25 +8527,34 @@ function HomeComponent() { }); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-hono-apollo-server

+

+ TanStack Router and hono, sqlite through drizzle, Better Auth. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -8996,6 +9206,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -9004,6 +9215,14 @@ export const accountRelations = relations(account, ({ one }) => ({ , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -9086,7 +9305,7 @@ export const accountRelations = relations(account, ({ one }) => ({ exports[`Template Snapshots Key File Content Snapshots key files: better-auth-full 1`] = ` { - "fileCount": 67, + "fileCount": 69, "files": [ { "content": "[exists]", @@ -9396,52 +9615,163 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import { Link } from "@tanstack/react-router"; + +import { authClient } from "@/lib/auth-client"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const { data: session, isPending: sessionPending } = authClient.useSession(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + enabled: Boolean(session), + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); + + if (sessionPending) { + return

Checking your session...

; + } + + if (!session) { + return ( +
+

+ Tasks are stored per account. Sign in to create your first one. +

+ + Sign in + +
+ ); + } return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-better-auth-full

+

+ TanStack Router and hono, postgres through drizzle, Better Auth. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -9682,6 +10012,14 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { protectedProcedure, publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-better-auth-full/db/tasks"; // @@ -9694,6 +10032,20 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: protectedProcedure.query(({ ctx }) => listTasks(ctx.session.user.id)), + create: protectedProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ ctx, input }) => createTask(ctx.session.user.id, input.title)), + setCompleted: protectedProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ ctx, input }) => + setTaskCompleted(ctx.session.user.id, input.id, input.completed), + ), + remove: protectedProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ ctx, input }) => deleteTask(ctx.session.user.id, input.id)), + }), privateData: protectedProcedure.query(({ ctx }) => { return { message: "This is private", @@ -10031,6 +10383,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -10039,6 +10392,14 @@ export const accountRelations = relations(account, ({ one }) => ({ , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -10307,51 +10668,46 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "convex/react"; + import { api } from "@snapshot-convex-clerk/backend/convex/_generated/api"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery(api.healthCheck.get); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck === undefined - ? "Checking..." - : healthCheck === "OK" - ? "Connected" - : "Error"} - -
-
-
+
+
+

snapshot-convex-clerk

+

+ TanStack Router and convex. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck === undefined + ? "Checking API..." + : healthCheck === "OK" + ? "API connected" + : "API error"} + +
); } @@ -10656,7 +11012,7 @@ export default defineSchema({}); exports[`Template Snapshots Key File Content Snapshots key files: self-next-clerk 1`] = ` { - "fileCount": 57, + "fileCount": 59, "files": [ { "content": "[exists]", @@ -11032,6 +11388,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-self-next-clerk/db/tasks"; // @@ -11044,6 +11408,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -11192,6 +11568,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -11200,6 +11577,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -11283,7 +11668,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: self-tanstack-start-clerk 1`] = ` { - "fileCount": 53, + "fileCount": 55, "files": [ { "content": "[exists]", @@ -11895,6 +12280,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -11903,6 +12289,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -12266,52 +12660,46 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-mongodb-mongoose

+

+ TanStack Router and hono, mongodb through mongoose. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -13005,52 +13393,46 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
-
-
+
+
+

snapshot-postgres-prisma

+

+ TanStack Router and hono, postgres through prisma. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -13483,7 +13865,7 @@ export default prisma; exports[`Template Snapshots Key File Content Snapshots key files: algolia-search-hono 1`] = ` { - "fileCount": 58, + "fileCount": 60, "files": [ { "content": "[exists]", @@ -13771,52 +14153,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-algolia-search-hono

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -14030,6 +14499,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-algolia-search-hono/db/tasks"; // @@ -14042,6 +14519,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -14190,6 +14679,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -14198,6 +14688,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -14280,7 +14778,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: opensearch-search-hono 1`] = ` { - "fileCount": 58, + "fileCount": 60, "files": [ { "content": "[exists]", @@ -14568,52 +15066,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-opensearch-search-hono

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -14827,6 +15412,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-opensearch-search-hono/db/tasks"; // @@ -14839,6 +15432,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -14987,6 +15592,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -14995,6 +15601,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -15077,7 +15691,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: cms-keystatic-next 1`] = ` { - "fileCount": 60, + "fileCount": 62, "files": [ { "content": "[exists]", @@ -15467,6 +16081,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-cms-keystatic-next/db/tasks"; // @@ -15479,6 +16101,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -15627,6 +16261,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -15635,6 +16270,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -15718,7 +16361,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: i18n-paraglide-tanstack-router 1`] = ` { - "fileCount": 60, + "fileCount": 62, "files": [ { "content": "[exists]", @@ -16014,52 +16657,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-i18n-paraglide-tanstack-router

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -16279,6 +17009,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-i18n-paraglide-tanstack-router/db/tasks"; // @@ -16291,6 +17029,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -16439,6 +17189,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -16447,6 +17198,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -16529,7 +17288,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: ai-cli-root-tooling 1`] = ` { - "fileCount": 58, + "fileCount": 60, "files": [ { "content": "[exists]", @@ -16816,52 +17575,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-ai-cli-root-tooling

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -17081,6 +17927,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-ai-cli-root-tooling/db/tasks"; // @@ -17093,6 +17947,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -17241,6 +18107,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -17249,6 +18116,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -17331,7 +18206,7 @@ export const db = drizzle({ client, schema }); exports[`Template Snapshots Key File Content Snapshots key files: nx-root-tooling 1`] = ` { - "fileCount": 58, + "fileCount": 60, "files": [ { "content": "[exists]", @@ -17614,52 +18489,139 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" /> - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} + + {item.title} -
-
-
+ + + ))} + + ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} + +function HomeComponent() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
+

snapshot-nx-root-tooling

+

+ TanStack Router and hono, sqlite through drizzle. +

+
+ +
+

Tasks

+ +
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -17878,6 +18840,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-nx-root-tooling/db/tasks"; // @@ -17890,6 +18860,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -18038,6 +19020,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -18046,6 +19029,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ @@ -18295,32 +19286,28 @@ export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
-
+
+
+

snapshot-frontend-only-no-backend

+

+ TanStack Router. +

+
+ +
+

Tasks

+

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+
+ +
+
); } @@ -18533,7 +19520,7 @@ export default defineConfig({ exports[`Template Snapshots Key File Content Snapshots key files: native-react-native 1`] = ` { - "fileCount": 67, + "fileCount": 69, "files": [ { "content": "[exists]", @@ -19109,6 +20096,14 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@snapshot-native-react-native/db/tasks"; // @@ -19121,6 +20116,18 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + tasks: router({ + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + }), }); export type AppRouter = typeof appRouter; " @@ -19269,6 +20276,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -19277,6 +20285,14 @@ export const db = drizzle({ client, schema }); , "path": "packages/db/src/schema/index.ts", }, + { + "content": "[exists]", + "path": "packages/db/src/schema/task.ts", + }, + { + "content": "[exists]", + "path": "packages/db/src/tasks.ts", + }, { "content": "{ diff --git a/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs b/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs index 45a511fb9..9f3179e94 100644 --- a/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs +++ b/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs @@ -29,6 +29,16 @@ import { {{#if (isBetterAuth auth)}}protectedProcedure, {{/if}}publicProcedure, router, } from "../index{{#if (eq backend "adonisjs")}}.js{{/if}}"; +{{#if (eq orm "drizzle")}} +import { z } from "zod"; + +import { + createTask, + deleteTask, + listTasks, + setTaskCompleted, +} from "@{{projectName}}/db/tasks"; +{{/if}} // @@ -41,6 +51,35 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), + {{#if (eq orm "drizzle")}} + tasks: router({ + {{#if (isBetterAuth auth)}} + list: protectedProcedure.query(({ ctx }) => listTasks(ctx.session.user.id)), + create: protectedProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ ctx, input }) => createTask(ctx.session.user.id, input.title)), + setCompleted: protectedProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ ctx, input }) => + setTaskCompleted(ctx.session.user.id, input.id, input.completed), + ), + remove: protectedProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ ctx, input }) => deleteTask(ctx.session.user.id, input.id)), + {{else}} + list: publicProcedure.query(() => listTasks()), + create: publicProcedure + .input(z.object({ title: z.string().trim().min(1).max(255) })) + .mutation(({ input }) => createTask(input.title)), + setCompleted: publicProcedure + .input(z.object({ id: z.number().int(), completed: z.boolean() })) + .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), + remove: publicProcedure + .input(z.object({ id: z.number().int() })) + .mutation(({ input }) => deleteTask(input.id)), + {{/if}} + }), + {{/if}} {{#if (isBetterAuth auth)}} privateData: protectedProcedure.query(({ ctx }) => { return { diff --git a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs index 9a6aa9b34..0e6c93574 100644 --- a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs @@ -3,6 +3,7 @@ export * from "./auth"; {{else}} export * from "./example"; {{/if}} +export * from "./task"; // diff --git a/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs new file mode 100644 index 000000000..460e9ef4b --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs @@ -0,0 +1,63 @@ +import { {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}and, {{/if}}desc, eq } from "drizzle-orm"; + +import { db } from "./index"; +import { task } from "./schema/task"; + +export type Task = typeof task.$inferSelect; + +{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +export function listTasks(userId: string) { + return db + .select() + .from(task) + .where(eq(task.userId, userId)) + .orderBy(desc(task.createdAt)); +} + +export async function createTask(userId: string, title: string): Promise { + {{#if (eq database "mysql")}} + const [inserted] = await db.insert(task).values({ title, userId }).$returningId(); + if (!inserted) throw new Error("Could not create the task"); + const [created] = await db.select().from(task).where(eq(task.id, inserted.id)); + {{else}} + const [created] = await db.insert(task).values({ title, userId }).returning(); + {{/if}} + if (!created) throw new Error("Could not create the task"); + return created; +} + +export async function setTaskCompleted(userId: string, id: number, completed: boolean) { + await db + .update(task) + .set({ completed }) + .where(and(eq(task.id, id), eq(task.userId, userId))); +} + +export async function deleteTask(userId: string, id: number) { + await db.delete(task).where(and(eq(task.id, id), eq(task.userId, userId))); +} +{{else}} +export function listTasks() { + return db.select().from(task).orderBy(desc(task.createdAt)); +} + +export async function createTask(title: string): Promise { + {{#if (eq database "mysql")}} + const [inserted] = await db.insert(task).values({ title }).$returningId(); + if (!inserted) throw new Error("Could not create the task"); + const [created] = await db.select().from(task).where(eq(task.id, inserted.id)); + {{else}} + const [created] = await db.insert(task).values({ title }).returning(); + {{/if}} + if (!created) throw new Error("Could not create the task"); + return created; +} + +export async function setTaskCompleted(id: number, completed: boolean) { + await db.update(task).set({ completed }).where(eq(task.id, id)); +} + +export async function deleteTask(id: number) { + await db.delete(task).where(eq(task.id, id)); +} +{{/if}} diff --git a/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs new file mode 100644 index 000000000..2730c14b9 --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs @@ -0,0 +1,27 @@ +{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +import { boolean, index, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; + +import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +{{else}} +import { boolean, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; +{{/if}} + +export const task = mysqlTable( + "task", + { + id: serial("id").primaryKey(), + title: varchar("title", { length: 255 }).notNull(), + completed: boolean("completed").default(false).notNull(), + {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} + userId: varchar("user_id", { length: {{#if (isBetterAuth auth)}}36{{else}}255{{/if}} }) + .notNull() + .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + {{/if}} + createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { fsp: 3 }) + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} +); diff --git a/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs new file mode 100644 index 000000000..f7cf76602 --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs @@ -0,0 +1,27 @@ +{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +import { boolean, index, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; + +import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +{{else}} +import { boolean, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; +{{/if}} + +export const task = pgTable( + "task", + { + id: serial("id").primaryKey(), + title: text("title").notNull(), + completed: boolean("completed").default(false).notNull(), + {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} + userId: text("user_id") + .notNull() + .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + {{/if}} + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} +); diff --git a/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs new file mode 100644 index 000000000..68099f00a --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs @@ -0,0 +1,30 @@ +import { sql } from "drizzle-orm"; +{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +import { sqliteTable, integer, text, index } from "drizzle-orm/sqlite-core"; + +import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +{{else}} +import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"; +{{/if}} + +export const task = sqliteTable( + "task", + { + id: integer("id").primaryKey({ autoIncrement: true }), + title: text("title").notNull(), + completed: integer("completed", { mode: "boolean" }).default(false).notNull(), + {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} + userId: text("user_id") + .notNull() + .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + {{/if}} + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} +); diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index 4e3362e02..3e5cbe973 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -1,45 +1,162 @@ -import { createFileRoute } from "@tanstack/react-router"; {{#if (eq backend "convex")}} +import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "convex/react"; -import { api } from "@{{ projectName }}/backend/convex/_generated/api"; -{{else if (eq api "orpc")}} -import { orpc } from "@/utils/orpc"; -import { useQuery } from "@tanstack/react-query"; -{{else if (eq api "trpc")}} -import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; + +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +{{else}} +import { createFileRoute } from "@tanstack/react-router"; +{{#if (or (eq api "orpc") (eq api "trpc"))}} +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; {{else if (eq api "ts-rest")}} import { tsr } from "@/utils/ts-rest"; -{{else if (eq api "garph")}} -import { query, resolved } from "@/utils/garph"; -import { useQuery } from "@tanstack/react-query"; -{{else if (or (eq api "graphql-yoga") (eq api "apollo-server"))}} -import { graphqlFetch } from "@/utils/graphql"; +{{else if (or (eq api "garph") (eq api "graphql-yoga") (eq api "apollo-server"))}} import { useQuery } from "@tanstack/react-query"; {{else if (eq api "openapi")}} -import { env } from "@{{projectName}}/env/web"; import { useEffect, useState } from "react"; {{/if}} +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{else if (eq api "trpc")}} +import { trpc } from "@/utils/trpc"; +{{/if}} +{{/if}} +{{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} +import { useState } from "react"; + +{{#if (isBetterAuth auth)}} +import { Link } from "@tanstack/react-router"; + +import { authClient } from "@/lib/auth-client"; +{{/if}} +{{/if}} export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = ` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - `; +{{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + {{#if (isBetterAuth auth)}} + const { data: session, isPending: sessionPending } = authClient.useSession(); + {{/if}} + + const tasks = useQuery({ + ...trpc.tasks.list.queryOptions(), + {{#if (isBetterAuth auth)}} + enabled: Boolean(session), + {{/if}} + }); + + const invalidate = () => + queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + + const create = useMutation({ + ...trpc.tasks.create.mutationOptions(), + onSuccess: () => { + setTitle(""); + return invalidate(); + }, + }); + const setCompleted = useMutation({ + ...trpc.tasks.setCompleted.mutationOptions(), + onSuccess: invalidate, + }); + const remove = useMutation({ + ...trpc.tasks.remove.mutationOptions(), + onSuccess: invalidate, + }); + + {{#if (isBetterAuth auth)}} + if (sessionPending) { + return

Checking your session...

; + } + + if (!session) { + return ( +
+

+ Tasks are stored per account. Sign in to create your first one. +

+ + Sign in + +
+ ); + } + {{/if}} + + return ( +
+
{ + event.preventDefault(); + const next = title.trim(); + if (next) create.mutate({ title: next }); + }} + > + setTitle(event.target.value)} + placeholder="Write your first task" + value={title} + /> + +
+ + {tasks.isPending ? ( +

Loading tasks...

+ ) : tasks.data && tasks.data.length > 0 ? ( +
    + {tasks.data.map((item) => ( +
  • + + setCompleted.mutate({ completed: event.target.checked, id: item.id }) + } + type="checkbox" + /> + + {item.title} + + +
  • + ))} +
+ ) : ( +

+ No tasks yet. Add one above, then reload the page. It is stored in your database. +

+ )} +
+ ); +} +{{/if}} function HomeComponent() { {{#if (eq backend "convex")}} @@ -78,42 +195,53 @@ function HomeComponent() { {{/if}} return ( -
-
{TITLE_TEXT}
-
-
-

API Status

- {{#if (eq backend "convex")}} -
-
- - {healthCheck === undefined - ? "Checking..." - : healthCheck === "OK" - ? "Connected" - : "Error"} - -
- {{else}} - {{#unless (eq api "none")}} -
-
- - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - -
- {{/unless}} - {{/if}} -
-
+
+
+

{{projectName}}

+

+ TanStack Router{{#unless (eq backend "none")}} and {{backend}}{{/unless}}{{#unless (eq database "none")}}, {{database}} through {{orm}}{{/unless}}{{#if (isBetterAuth auth)}}, Better Auth{{/if}}. +

+
+ +
+

Tasks

+ {{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} + + {{else}} +

+ This stack has no database-backed task API yet. Replace this panel with your first + feature, or scaffold one with bfs gen resource task. +

+ {{/if}} +
+ +
+ {{#if (eq backend "convex")}} + + + {healthCheck === undefined + ? "Checking API..." + : healthCheck === "OK" + ? "API connected" + : "API error"} + + {{else}} + {{#unless (eq api "none")}} + + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + + {{/unless}} + {{/if}} +
); } From 447f9c4a8a30fce78b6319dee1ca751e3be47bb9 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 14:24:57 +0300 Subject: [PATCH 2/7] fix(templates): restrict task routes to supported auth and restore health imports Review findings on the generated first screen, all three reviewers converging on the same defects. isBetterAuth matches only two of the twelve auth values, so the broad else branch gave the other providers a public, globally writable task list inside an app that has authentication, and nextauth additionally failed to compile because the repository takes a user id the router never passed. Tasks are now generated only for Better Auth and no-auth stacks; every other provider gets the same honest fallback panel as a stack without a task API. The rewritten import block dropped the helpers the other API branches use, so garph, graphql-yoga, apollo-server and openapi stacks referenced query, resolved, graphqlFetch and env without importing them. All three import sets are restored, and each branch now imports only what it uses so orpc and non-Drizzle tRPC stacks carry no unused mutation helpers. Cached tasks are dropped whenever the signed-in user changes, so one session cannot show another session's rows. Failed mutations render an alert instead of failing silently, and the title input caps at the 255 characters the router validates. Verified by generating and type-checking better-auth, nextauth, clerk, no-auth and openapi stacks. --- .../template-snapshots.test.ts.snap | 155 +++++++++++------- .../api/trpc/server/src/routers/index.ts.hbs | 4 +- .../db/drizzle/base/src/schema/index.ts.hbs | 2 + .../db/drizzle/base/src/tasks.ts.hbs | 6 +- .../db/drizzle/mysql/src/schema/task.ts.hbs | 14 +- .../drizzle/postgres/src/schema/task.ts.hbs | 12 +- .../db/drizzle/sqlite/src/schema/task.ts.hbs | 12 +- .../tanstack-router/src/routes/index.tsx.hbs | 67 +++++--- 8 files changed, 172 insertions(+), 100 deletions(-) diff --git a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap index 103a8bb0c..5ac347022 100644 --- a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap @@ -588,8 +588,6 @@ exports[`Template Snapshots File Structure Snapshots file structure: typescript- "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", - "packages/db/src/schema/task.ts", - "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -901,8 +899,6 @@ exports[`Template Snapshots File Structure Snapshots file structure: self-next-c "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", - "packages/db/src/schema/task.ts", - "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -957,8 +953,6 @@ exports[`Template Snapshots File Structure Snapshots file structure: self-tansta "packages/db/src/index.ts", "packages/db/src/schema/example.ts", "packages/db/src/schema/index.ts", - "packages/db/src/schema/task.ts", - "packages/db/src/tasks.ts", "packages/db/tsconfig.json", "packages/env/package.json", "packages/env/src/native.ts", @@ -2059,9 +2053,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -2094,6 +2088,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -2108,6 +2104,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -2121,6 +2118,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -5762,7 +5765,8 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; + import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ @@ -6557,7 +6561,8 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; + import { orpc } from "@/utils/orpc"; export const Route = createFileRoute("/")({ @@ -7419,6 +7424,8 @@ if (!rootElement.innerHTML) { "import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; +import { env } from "@snapshot-hono-openapi/env/web"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); @@ -8515,6 +8522,8 @@ if (!rootElement.innerHTML) { "import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; +import { graphqlFetch } from "@/utils/graphql"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); @@ -9616,12 +9625,11 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; -import { useState } from "react"; - import { Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; import { authClient } from "@/lib/auth-client"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -9631,10 +9639,17 @@ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); const { data: session, isPending: sessionPending } = authClient.useSession(); + const userId = session?.user.id; + + // Drop cached rows whenever the signed-in user changes so one session never + // shows another session's tasks. + useEffect(() => { + queryClient.removeQueries({ queryKey: trpc.tasks.list.queryKey() }); + }, [queryClient, userId]); const tasks = useQuery({ ...trpc.tasks.list.queryOptions(), - enabled: Boolean(session), + enabled: Boolean(userId), }); const invalidate = () => @@ -9656,11 +9671,13 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + if (sessionPending) { return

Checking your session...

; } - if (!session) { + if (!userId) { return (

@@ -9689,6 +9706,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -9702,6 +9720,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -11012,7 +11036,7 @@ export default defineSchema({}); exports[`Template Snapshots Key File Content Snapshots key files: self-next-clerk 1`] = ` { - "fileCount": 59, + "fileCount": 57, "files": [ { "content": "[exists]", @@ -11388,14 +11412,6 @@ export const publicProcedure = t.procedure; publicProcedure, router, } from "../index"; -import { z } from "zod"; - -import { - createTask, - deleteTask, - listTasks, - setTaskCompleted, -} from "@snapshot-self-next-clerk/db/tasks"; // @@ -11408,18 +11424,6 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), - tasks: router({ - list: publicProcedure.query(() => listTasks()), - create: publicProcedure - .input(z.object({ title: z.string().trim().min(1).max(255) })) - .mutation(({ input }) => createTask(input.title)), - setCompleted: publicProcedure - .input(z.object({ id: z.number().int(), completed: z.boolean() })) - .mutation(({ input }) => setTaskCompleted(input.id, input.completed)), - remove: publicProcedure - .input(z.object({ id: z.number().int() })) - .mutation(({ input }) => deleteTask(input.id)), - }), }); export type AppRouter = typeof appRouter; " @@ -11568,7 +11572,6 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; -export * from "./task"; // @@ -11577,14 +11580,6 @@ export * from "./task"; , "path": "packages/db/src/schema/index.ts", }, - { - "content": "[exists]", - "path": "packages/db/src/schema/task.ts", - }, - { - "content": "[exists]", - "path": "packages/db/src/tasks.ts", - }, { "content": "{ @@ -11668,7 +11663,7 @@ export * from "./task"; exports[`Template Snapshots Key File Content Snapshots key files: self-tanstack-start-clerk 1`] = ` { - "fileCount": 55, + "fileCount": 53, "files": [ { "content": "[exists]", @@ -12280,7 +12275,6 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; -export * from "./task"; // @@ -12289,14 +12283,6 @@ export * from "./task"; , "path": "packages/db/src/schema/index.ts", }, - { - "content": "[exists]", - "path": "packages/db/src/schema/task.ts", - }, - { - "content": "[exists]", - "path": "packages/db/src/tasks.ts", - }, { "content": "{ @@ -12660,7 +12646,8 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; + import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ @@ -13393,7 +13380,8 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; + import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ @@ -14154,9 +14142,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -14189,6 +14177,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -14203,6 +14193,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -14216,6 +14207,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -15067,9 +15064,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -15102,6 +15099,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -15116,6 +15115,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -15129,6 +15129,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -16658,9 +16664,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -16693,6 +16699,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -16707,6 +16715,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -16720,6 +16729,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -17576,9 +17591,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -17611,6 +17626,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -17625,6 +17642,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -17638,6 +17656,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -18490,9 +18514,9 @@ if (!rootElement.innerHTML) { "content": "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { trpc } from "@/utils/trpc"; import { useState } from "react"; +import { trpc } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -18525,6 +18549,8 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + return (
@@ -18539,6 +18565,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -18552,6 +18579,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( diff --git a/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs b/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs index 9f3179e94..cadf32e98 100644 --- a/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs +++ b/packages/template-generator/templates/api/trpc/server/src/routers/index.ts.hbs @@ -29,7 +29,7 @@ import { {{#if (isBetterAuth auth)}}protectedProcedure, {{/if}}publicProcedure, router, } from "../index{{#if (eq backend "adonisjs")}}.js{{/if}}"; -{{#if (eq orm "drizzle")}} +{{#if (and (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} import { z } from "zod"; import { @@ -51,7 +51,7 @@ export const appRouter = router({ healthCheck: publicProcedure.query(() => { return "OK"; }), - {{#if (eq orm "drizzle")}} + {{#if (and (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} tasks: router({ {{#if (isBetterAuth auth)}} list: protectedProcedure.query(({ ctx }) => listTasks(ctx.session.user.id)), diff --git a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs index 0e6c93574..53282e44b 100644 --- a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs @@ -3,7 +3,9 @@ export * from "./auth"; {{else}} export * from "./example"; {{/if}} +{{#if (or (isBetterAuth auth) (eq auth "none"))}} export * from "./task"; +{{/if}} // diff --git a/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs index 460e9ef4b..9cb6f95a8 100644 --- a/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs @@ -1,11 +1,12 @@ -import { {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}and, {{/if}}desc, eq } from "drizzle-orm"; +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +import { {{#if (isBetterAuth auth)}}and, {{/if}}desc, eq } from "drizzle-orm"; import { db } from "./index"; import { task } from "./schema/task"; export type Task = typeof task.$inferSelect; -{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +{{#if (isBetterAuth auth)}} export function listTasks(userId: string) { return db .select() @@ -61,3 +62,4 @@ export async function deleteTask(id: number) { await db.delete(task).where(eq(task.id, id)); } {{/if}} +{{/if}} diff --git a/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs index 2730c14b9..edfd8fa2c 100644 --- a/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs @@ -1,7 +1,8 @@ -{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +{{#if (isBetterAuth auth)}} import { boolean, index, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; -import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +import { user } from "./auth"; {{else}} import { boolean, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; {{/if}} @@ -12,16 +13,17 @@ export const task = mysqlTable( id: serial("id").primaryKey(), title: varchar("title", { length: 255 }).notNull(), completed: boolean("completed").default(false).notNull(), - {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} - userId: varchar("user_id", { length: {{#if (isBetterAuth auth)}}36{{else}}255{{/if}} }) + {{#if (isBetterAuth auth)}} + userId: varchar("user_id", { length: 36 }) .notNull() - .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + .references(() => user.id, { onDelete: "cascade" }), {{/if}} createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { fsp: 3 }) .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), - }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + }{{#if (isBetterAuth auth)}}, (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} ); +{{/if}} diff --git a/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs index f7cf76602..ceba0e24a 100644 --- a/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs @@ -1,7 +1,8 @@ -{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +{{#if (isBetterAuth auth)}} import { boolean, index, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; -import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +import { user } from "./auth"; {{else}} import { boolean, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; {{/if}} @@ -12,16 +13,17 @@ export const task = pgTable( id: serial("id").primaryKey(), title: text("title").notNull(), completed: boolean("completed").default(false).notNull(), - {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} + {{#if (isBetterAuth auth)}} userId: text("user_id") .notNull() - .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + .references(() => user.id, { onDelete: "cascade" }), {{/if}} createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), - }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + }{{#if (isBetterAuth auth)}}, (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} ); +{{/if}} diff --git a/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs index 68099f00a..605bcd59b 100644 --- a/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs @@ -1,8 +1,9 @@ +{{#if (or (isBetterAuth auth) (eq auth "none"))}} import { sql } from "drizzle-orm"; -{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} +{{#if (isBetterAuth auth)}} import { sqliteTable, integer, text, index } from "drizzle-orm/sqlite-core"; -import { {{#if (isBetterAuth auth)}}user{{else}}users{{/if}} } from "./auth"; +import { user } from "./auth"; {{else}} import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"; {{/if}} @@ -13,10 +14,10 @@ export const task = sqliteTable( id: integer("id").primaryKey({ autoIncrement: true }), title: text("title").notNull(), completed: integer("completed", { mode: "boolean" }).default(false).notNull(), - {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} + {{#if (isBetterAuth auth)}} userId: text("user_id") .notNull() - .references(() => {{#if (isBetterAuth auth)}}user{{else}}users{{/if}}.id, { onDelete: "cascade" }), + .references(() => user.id, { onDelete: "cascade" }), {{/if}} createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) @@ -25,6 +26,7 @@ export const task = sqliteTable( .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), - }{{#if (or (isBetterAuth auth) (eq auth "nextauth"))}}, + }{{#if (isBetterAuth auth)}}, (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} ); +{{/if}} diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index 3e5cbe973..ddef49601 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -5,47 +5,67 @@ import { useQuery } from "convex/react"; import { api } from "@{{projectName}}/backend/convex/_generated/api"; {{else}} import { createFileRoute } from "@tanstack/react-router"; -{{#if (or (eq api "orpc") (eq api "trpc"))}} -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -{{else if (eq api "ts-rest")}} -import { tsr } from "@/utils/ts-rest"; -{{else if (or (eq api "garph") (eq api "graphql-yoga") (eq api "apollo-server"))}} -import { useQuery } from "@tanstack/react-query"; -{{else if (eq api "openapi")}} -import { useEffect, useState } from "react"; -{{/if}} {{#if (eq api "orpc")}} +import { useQuery } from "@tanstack/react-query"; + import { orpc } from "@/utils/orpc"; {{else if (eq api "trpc")}} -import { trpc } from "@/utils/trpc"; -{{/if}} -{{/if}} -{{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} -import { useState } from "react"; - +{{#if (and (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; {{#if (isBetterAuth auth)}} import { Link } from "@tanstack/react-router"; +{{/if}} +import { {{#if (isBetterAuth auth)}}useEffect, {{/if}}useState } from "react"; +{{#if (isBetterAuth auth)}} import { authClient } from "@/lib/auth-client"; {{/if}} +import { trpc } from "@/utils/trpc"; +{{else}} +import { useQuery } from "@tanstack/react-query"; + +import { trpc } from "@/utils/trpc"; +{{/if}} +{{else if (eq api "ts-rest")}} +import { tsr } from "@/utils/ts-rest"; +{{else if (eq api "garph")}} +import { useQuery } from "@tanstack/react-query"; + +import { query, resolved } from "@/utils/garph"; +{{else if (or (eq api "graphql-yoga") (eq api "apollo-server"))}} +import { useQuery } from "@tanstack/react-query"; + +import { graphqlFetch } from "@/utils/graphql"; +{{else if (eq api "openapi")}} +import { useEffect, useState } from "react"; + +import { env } from "@{{projectName}}/env/web"; +{{/if}} {{/if}} export const Route = createFileRoute("/")({ component: HomeComponent, }); -{{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} +{{#if (and (eq api "trpc") (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); {{#if (isBetterAuth auth)}} const { data: session, isPending: sessionPending } = authClient.useSession(); + const userId = session?.user.id; + + // Drop cached rows whenever the signed-in user changes so one session never + // shows another session's tasks. + useEffect(() => { + queryClient.removeQueries({ queryKey: trpc.tasks.list.queryKey() }); + }, [queryClient, userId]); {{/if}} const tasks = useQuery({ ...trpc.tasks.list.queryOptions(), {{#if (isBetterAuth auth)}} - enabled: Boolean(session), + enabled: Boolean(userId), {{/if}} }); @@ -68,12 +88,14 @@ function TaskPanel() { onSuccess: invalidate, }); + const mutationError = create.error ?? setCompleted.error ?? remove.error; + {{#if (isBetterAuth auth)}} if (sessionPending) { return

Checking your session...

; } - if (!session) { + if (!userId) { return (

@@ -103,6 +125,7 @@ function TaskPanel() { setTitle(event.target.value)} placeholder="Write your first task" value={title} @@ -116,6 +139,12 @@ function TaskPanel() { + {mutationError ? ( +

+ {mutationError.message} +

+ ) : null} + {tasks.isPending ? (

Loading tasks...

) : tasks.data && tasks.data.length > 0 ? ( @@ -205,7 +234,7 @@ function HomeComponent() {

Tasks

- {{#if (and (eq orm "drizzle") (eq api "trpc") (not (eq backend "convex")))}} + {{#if (and (eq api "trpc") (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} {{else}}

From b7f536414928ca042a40368c254e037e15343c13 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 14:40:55 +0300 Subject: [PATCH 3/7] fix(templates): show task load failures and drop the unsupported command hint Two review findings on the first screen. A failed task query fell through to the empty state, so a missing table or an unreachable database read as "No tasks yet" rather than an error. The panel now renders the failure and points at the migration. The fallback panel advertised `bfs gen resource task`, which is wrong twice over: there is no bfs binary, and gen returns unsupported when no recipe adapter matches the stack graph, which is exactly the case for the OpenAPI and GraphQL stacks that see this panel. The sentence now just says the stack has no database-backed task API yet. --- .../template-snapshots.test.ts.snap | 44 +++++++++++++++---- .../tanstack-router/src/routes/index.tsx.hbs | 6 ++- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap index 5ac347022..abeb18fdf 100644 --- a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap @@ -2126,6 +2126,10 @@ function TaskPanel() { {tasks.isPending ? (

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

) : tasks.data && tasks.data.length > 0 ? (
    {tasks.data.map((item) => ( @@ -5790,7 +5794,7 @@ function HomeComponent() {

    Tasks

    This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

@@ -6586,7 +6590,7 @@ function HomeComponent() {

Tasks

This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

@@ -7460,7 +7464,7 @@ function HomeComponent() {

Tasks

This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

@@ -8548,7 +8552,7 @@ function HomeComponent() {

Tasks

This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

@@ -9728,6 +9732,10 @@ function TaskPanel() { {tasks.isPending ? (

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

) : tasks.data && tasks.data.length > 0 ? (
    {tasks.data.map((item) => ( @@ -10716,7 +10724,7 @@ function HomeComponent() {

    Tasks

    This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

    @@ -12671,7 +12679,7 @@ function HomeComponent() {

    Tasks

    This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

    @@ -13405,7 +13413,7 @@ function HomeComponent() {

    Tasks

    This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

    @@ -14215,6 +14223,10 @@ function TaskPanel() { {tasks.isPending ? (

    Loading tasks...

    + ) : tasks.isError ? ( +

    + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

    ) : tasks.data && tasks.data.length > 0 ? (
      {tasks.data.map((item) => ( @@ -15137,6 +15149,10 @@ function TaskPanel() { {tasks.isPending ? (

      Loading tasks...

      + ) : tasks.isError ? ( +

      + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

      ) : tasks.data && tasks.data.length > 0 ? (
        {tasks.data.map((item) => ( @@ -16737,6 +16753,10 @@ function TaskPanel() { {tasks.isPending ? (

        Loading tasks...

        + ) : tasks.isError ? ( +

        + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

        ) : tasks.data && tasks.data.length > 0 ? (
          {tasks.data.map((item) => ( @@ -17664,6 +17684,10 @@ function TaskPanel() { {tasks.isPending ? (

          Loading tasks...

          + ) : tasks.isError ? ( +

          + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

          ) : tasks.data && tasks.data.length > 0 ? (
            {tasks.data.map((item) => ( @@ -18587,6 +18611,10 @@ function TaskPanel() { {tasks.isPending ? (

            Loading tasks...

            + ) : tasks.isError ? ( +

            + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

            ) : tasks.data && tasks.data.length > 0 ? (
              {tasks.data.map((item) => ( @@ -19335,7 +19363,7 @@ function HomeComponent() {

              Tasks

              This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

              diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index ddef49601..b314d02c3 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -147,6 +147,10 @@ function TaskPanel() { {tasks.isPending ? (

              Loading tasks...

              + ) : tasks.isError ? ( +

              + Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) +

              ) : tasks.data && tasks.data.length > 0 ? (
                {tasks.data.map((item) => ( @@ -239,7 +243,7 @@ function HomeComponent() { {{else}}

                This stack has no database-backed task API yet. Replace this panel with your first - feature, or scaffold one with bfs gen resource task. + feature.

                {{/if}} From 074dd39734e7c1f24e0217ee27293583ff1464b0 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 14:51:43 +0300 Subject: [PATCH 4/7] fix(templates): give the drizzle import chain AdonisJS file extensions AdonisJS type-checks with NodeNext resolution, which rejects extensionless relative imports. The new tasks repository tripped TS2835 on both of its imports in an AdonisJS, Drizzle and tRPC project. Fixing those surfaced the same latent problem one level deeper: the db package entry point and schema barrel have always used extensionless relative imports, but AdonisJS never reached those files, so they never failed. Importing the repository pulls the whole chain into the AdonisJS program. Every relative import in that chain now carries the repository's existing adonisjs .js conditional. The task load failure also claimed a missing migration for every error, including an unreachable database, so it now reports the actual error. Verified by generating and type-checking AdonisJS alongside better-auth, nextauth, clerk, no-auth and openapi stacks. --- .../__snapshots__/template-snapshots.test.ts.snap | 14 +++++++------- .../db/drizzle/base/src/schema/index.ts.hbs | 6 +++--- .../templates/db/drizzle/base/src/tasks.ts.hbs | 4 ++-- .../templates/db/drizzle/mysql/src/index.ts.hbs | 4 ++-- .../db/drizzle/mysql/src/schema/task.ts.hbs | 2 +- .../templates/db/drizzle/postgres/src/index.ts.hbs | 4 ++-- .../db/drizzle/postgres/src/schema/task.ts.hbs | 2 +- .../templates/db/drizzle/sqlite/src/index.ts.hbs | 4 ++-- .../db/drizzle/sqlite/src/schema/task.ts.hbs | 2 +- .../react/tanstack-router/src/routes/index.tsx.hbs | 2 +- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap index abeb18fdf..6e95a8785 100644 --- a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap @@ -2128,7 +2128,7 @@ function TaskPanel() {

                Loading tasks...

                ) : tasks.isError ? (

                - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                ) : tasks.data && tasks.data.length > 0 ? (
                  @@ -9734,7 +9734,7 @@ function TaskPanel() {

                  Loading tasks...

                  ) : tasks.isError ? (

                  - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                  ) : tasks.data && tasks.data.length > 0 ? (
                    @@ -14225,7 +14225,7 @@ function TaskPanel() {

                    Loading tasks...

                    ) : tasks.isError ? (

                    - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                    ) : tasks.data && tasks.data.length > 0 ? (
                      @@ -15151,7 +15151,7 @@ function TaskPanel() {

                      Loading tasks...

                      ) : tasks.isError ? (

                      - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                      ) : tasks.data && tasks.data.length > 0 ? (
                        @@ -16755,7 +16755,7 @@ function TaskPanel() {

                        Loading tasks...

                        ) : tasks.isError ? (

                        - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                        ) : tasks.data && tasks.data.length > 0 ? (
                          @@ -17686,7 +17686,7 @@ function TaskPanel() {

                          Loading tasks...

                          ) : tasks.isError ? (

                          - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                          ) : tasks.data && tasks.data.length > 0 ? (
                            @@ -18613,7 +18613,7 @@ function TaskPanel() {

                            Loading tasks...

                            ) : tasks.isError ? (

                            - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                            ) : tasks.data && tasks.data.length > 0 ? (
                              diff --git a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs index 53282e44b..da8b6d612 100644 --- a/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/base/src/schema/index.ts.hbs @@ -1,10 +1,10 @@ {{#if (or (isBetterAuth auth) (eq auth "nextauth"))}} -export * from "./auth"; +export * from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{else}} -export * from "./example"; +export * from "./example{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{/if}} {{#if (or (isBetterAuth auth) (eq auth "none"))}} -export * from "./task"; +export * from "./task{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{/if}} // diff --git a/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs index 9cb6f95a8..74cd7d07d 100644 --- a/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs @@ -1,8 +1,8 @@ {{#if (or (isBetterAuth auth) (eq auth "none"))}} import { {{#if (isBetterAuth auth)}}and, {{/if}}desc, eq } from "drizzle-orm"; -import { db } from "./index"; -import { task } from "./schema/task"; +import { db } from "./index{{#if (eq backend "adonisjs")}}.js{{/if}}"; +import { task } from "./schema/task{{#if (eq backend "adonisjs")}}.js{{/if}}"; export type Task = typeof task.$inferSelect; diff --git a/packages/template-generator/templates/db/drizzle/mysql/src/index.ts.hbs b/packages/template-generator/templates/db/drizzle/mysql/src/index.ts.hbs index f040ddc92..d658afe0e 100644 --- a/packages/template-generator/templates/db/drizzle/mysql/src/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/mysql/src/index.ts.hbs @@ -1,6 +1,6 @@ {{#if (or (eq runtime "bun") (eq runtime "node") (eq runtime "none"))}} import { env } from "@{{projectName}}/env/server"; -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{#if (eq dbSetup "planetscale")}} import { drizzle } from "drizzle-orm/planetscale-serverless"; @@ -25,7 +25,7 @@ export const db = drizzle({ {{/if}} {{#if (eq runtime "workers")}} -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{#if (eq dbSetup "planetscale")}} import { drizzle } from "drizzle-orm/planetscale-serverless"; diff --git a/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs index edfd8fa2c..c45fd4755 100644 --- a/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs @@ -2,7 +2,7 @@ {{#if (isBetterAuth auth)}} import { boolean, index, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; -import { user } from "./auth"; +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{else}} import { boolean, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; {{/if}} diff --git a/packages/template-generator/templates/db/drizzle/postgres/src/index.ts.hbs b/packages/template-generator/templates/db/drizzle/postgres/src/index.ts.hbs index 1e2540ade..44ff2b9a9 100644 --- a/packages/template-generator/templates/db/drizzle/postgres/src/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/postgres/src/index.ts.hbs @@ -1,6 +1,6 @@ {{#if (or (eq runtime "bun") (eq runtime "node") (eq runtime "none"))}} import { env } from "@{{projectName}}/env/server"; -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{#if (eq dbSetup "neon")}} import { neon } from '@neondatabase/serverless'; @@ -16,7 +16,7 @@ export const db = drizzle(env.DATABASE_URL, { schema }); {{/if}} {{#if (eq runtime "workers")}} -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{#if (eq dbSetup "neon")}} import { neon } from '@neondatabase/serverless'; diff --git a/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs index ceba0e24a..fd805f0c6 100644 --- a/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs @@ -2,7 +2,7 @@ {{#if (isBetterAuth auth)}} import { boolean, index, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; -import { user } from "./auth"; +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{else}} import { boolean, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; {{/if}} diff --git a/packages/template-generator/templates/db/drizzle/sqlite/src/index.ts.hbs b/packages/template-generator/templates/db/drizzle/sqlite/src/index.ts.hbs index b50a73b5b..d548d97de 100644 --- a/packages/template-generator/templates/db/drizzle/sqlite/src/index.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/sqlite/src/index.ts.hbs @@ -1,6 +1,6 @@ {{#if (or (eq runtime "bun") (eq runtime "node") (eq runtime "none"))}} import { env } from "@{{projectName}}/env/server"; -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; import { drizzle } from "drizzle-orm/libsql"; import { createClient } from "@libsql/client"; @@ -15,7 +15,7 @@ export const db = drizzle({ client, schema }); {{/if}} {{#if (eq runtime "workers")}} -import * as schema from "./schema/index"; +import * as schema from "./schema/index{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{#if (eq dbSetup "d1")}} import { drizzle } from "drizzle-orm/d1"; diff --git a/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs index 605bcd59b..185d514d6 100644 --- a/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs +++ b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs @@ -3,7 +3,7 @@ import { sql } from "drizzle-orm"; {{#if (isBetterAuth auth)}} import { sqliteTable, integer, text, index } from "drizzle-orm/sqlite-core"; -import { user } from "./auth"; +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; {{else}} import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"; {{/if}} diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index b314d02c3..30e39294f 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -149,7 +149,7 @@ function TaskPanel() {

                              Loading tasks...

                              ) : tasks.isError ? (

                              - Could not load tasks. Run the database migration, then reload. ({tasks.error.message}) + Could not load tasks: {tasks.error.message}

                              ) : tasks.data && tasks.data.length > 0 ? (
                                From 8d0df1e407ace1696f986f228d70952a54b7ab70 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 14:59:43 +0300 Subject: [PATCH 5/7] fix(templates): put the user id in the task cache key Clearing cached rows in an effect ran after render, so a direct move from one signed-in user to another could paint the previous account's tasks for a frame before the effect cleared them. The query now carries the user id in its own key and fetches through the vanilla tRPC client, so each account has its own cache entry and stale rows can never be read. That also removes the effect entirely. --- .../template-snapshots.test.ts.snap | 66 ++++++++----------- .../tanstack-router/src/routes/index.tsx.hbs | 26 ++++---- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap index 6e95a8785..0bcc785de 100644 --- a/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/support/__snapshots__/template-snapshots.test.ts.snap @@ -2064,13 +2064,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -9630,10 +9628,10 @@ if (!rootElement.innerHTML) { "import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { authClient } from "@/lib/auth-client"; -import { trpc } from "@/utils/trpc"; +import { trpc, trpcClient } from "@/utils/trpc"; export const Route = createFileRoute("/")({ component: HomeComponent, @@ -9645,19 +9643,17 @@ function TaskPanel() { const { data: session, isPending: sessionPending } = authClient.useSession(); const userId = session?.user.id; - // Drop cached rows whenever the signed-in user changes so one session never - // shows another session's tasks. - useEffect(() => { - queryClient.removeQueries({ queryKey: trpc.tasks.list.queryKey() }); - }, [queryClient, userId]); + // The user id is part of the cache key, so a session change can never render + // rows that were fetched for the previous account. + const tasksQueryKey = ["tasks", "list", userId] as const; const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), + queryKey: tasksQueryKey, + queryFn: () => trpcClient.tasks.list.query(), enabled: Boolean(userId), }); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -14161,13 +14157,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -15087,13 +15081,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -16691,13 +16683,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -17622,13 +17612,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), @@ -18549,13 +18537,11 @@ export const Route = createFileRoute("/")({ function TaskPanel() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - }); + const tasks = useQuery(trpc.tasks.list.queryOptions()); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index 30e39294f..f7c099097 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -15,12 +15,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; {{#if (isBetterAuth auth)}} import { Link } from "@tanstack/react-router"; {{/if}} -import { {{#if (isBetterAuth auth)}}useEffect, {{/if}}useState } from "react"; +import { useState } from "react"; {{#if (isBetterAuth auth)}} import { authClient } from "@/lib/auth-client"; {{/if}} -import { trpc } from "@/utils/trpc"; +import { trpc{{#if (isBetterAuth auth)}}, trpcClient{{/if}} } from "@/utils/trpc"; {{else}} import { useQuery } from "@tanstack/react-query"; @@ -55,22 +55,22 @@ function TaskPanel() { const { data: session, isPending: sessionPending } = authClient.useSession(); const userId = session?.user.id; - // Drop cached rows whenever the signed-in user changes so one session never - // shows another session's tasks. - useEffect(() => { - queryClient.removeQueries({ queryKey: trpc.tasks.list.queryKey() }); - }, [queryClient, userId]); - {{/if}} + // The user id is part of the cache key, so a session change can never render + // rows that were fetched for the previous account. + const tasksQueryKey = ["tasks", "list", userId] as const; const tasks = useQuery({ - ...trpc.tasks.list.queryOptions(), - {{#if (isBetterAuth auth)}} + queryKey: tasksQueryKey, + queryFn: () => trpcClient.tasks.list.query(), enabled: Boolean(userId), - {{/if}} }); + {{else}} + const tasksQueryKey = trpc.tasks.list.queryKey(); + + const tasks = useQuery(trpc.tasks.list.queryOptions()); + {{/if}} - const invalidate = () => - queryClient.invalidateQueries({ queryKey: trpc.tasks.list.queryKey() }); + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); const create = useMutation({ ...trpc.tasks.create.mutationOptions(), From f429abdf5d5304e2ab58545254dc9ba904332340 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 21:30:26 +0300 Subject: [PATCH 6/7] fix(templates): hide absent ORM in first screen --- .../tanstack-router/src/routes/index.tsx.hbs | 2 +- .../generator-output-cleanliness.test.ts | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs index f7c099097..23f8e2061 100644 --- a/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +++ b/packages/template-generator/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs @@ -232,7 +232,7 @@ function HomeComponent() {

                                {{projectName}}

                                - TanStack Router{{#unless (eq backend "none")}} and {{backend}}{{/unless}}{{#unless (eq database "none")}}, {{database}} through {{orm}}{{/unless}}{{#if (isBetterAuth auth)}}, Better Auth{{/if}}. + TanStack Router{{#unless (eq backend "none")}} and {{backend}}{{/unless}}{{#unless (eq database "none")}}, {{database}}{{#unless (eq orm "none")}} through {{orm}}{{/unless}}{{/unless}}{{#if (isBetterAuth auth)}}, Better Auth{{/if}}.

                                diff --git a/packages/template-generator/test/output/generator-output-cleanliness.test.ts b/packages/template-generator/test/output/generator-output-cleanliness.test.ts index 7d00543c9..49f64cb2a 100644 --- a/packages/template-generator/test/output/generator-output-cleanliness.test.ts +++ b/packages/template-generator/test/output/generator-output-cleanliness.test.ts @@ -1,12 +1,12 @@ import { parseStackPartSpecs, type ProjectConfig } from "@better-fullstack/types"; +import { makeConfig } from "@test/_fixtures/config-factory"; import { describe, expect, it } from "bun:test"; import type { VirtualFile, VirtualNode } from "@/types"; +import { dependencyVersionMap } from "@/dependencies/add-deps"; import { generateVirtualProject } from "@/generator"; import { EMBEDDED_TEMPLATES } from "@/templates.generated"; -import { dependencyVersionMap } from "@/dependencies/add-deps"; -import { makeConfig } from "@test/_fixtures/config-factory"; function listFiles(node: VirtualNode): VirtualFile[] { return node.type === "file" ? [node] : node.children.flatMap(listFiles); @@ -58,6 +58,23 @@ describe("generated output cleanliness", () => { }); } + for (const database of ["redis", "edgedb"] as const) { + it(`does not expose the ORM sentinel for ${database}`, async () => { + const result = await generateVirtualProject({ + config: makeConfig({ database, orm: "none" }), + templates: EMBEDDED_TEMPLATES, + }); + + expect(result.success).toBe(true); + const route = listFiles(result.tree!.root).find((file) => + file.path.endsWith("apps/web/src/routes/index.tsx"), + ); + + expect(route?.content).toContain(`TanStack Router and hono, ${database}.`); + expect(route?.content).not.toContain(`${database} through none`); + }); + } + it("renders graph-native Blazor and Kotlin application templates", async () => { const config = makeConfig({ stackParts: parseStackPartSpecs([ From 7e97b9ce509999e3701a96ed14cd33678eeea53b Mon Sep 17 00:00:00 2001 From: Marve10s Date: Tue, 1 Sep 2026 21:40:00 +0300 Subject: [PATCH 7/7] fix(cli): refresh Bun cache after registry miss --- .../src/helpers/core/install-dependencies.ts | 38 +++++++++++++++---- .../generation/install-dependencies.test.ts | 17 +++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/apps/cli/src/helpers/core/install-dependencies.ts b/apps/cli/src/helpers/core/install-dependencies.ts index 6433cd539..c6832bef0 100644 --- a/apps/cli/src/helpers/core/install-dependencies.ts +++ b/apps/cli/src/helpers/core/install-dependencies.ts @@ -66,6 +66,19 @@ export function getInstallArgs(packageManager: PackageManager): string[] { return ["install"]; } +export function getInstallRetryArgs( + packageManager: PackageManager, + error: unknown, +): string[] | null { + if (packageManager !== "bun") return null; + + const stderr = (error as { stderr?: unknown })?.stderr; + const output = typeof stderr === "string" ? stderr : toErrorMessage(error); + return /No version matching .+ found for specifier .+\(but package exists\)/is.test(output) + ? ["install", "--force"] + : null; +} + export async function installDependencies({ projectDir, packageManager, @@ -81,14 +94,23 @@ export async function installDependencies({ s.start(`Running ${packageManager} install...`); const installArgs = getInstallArgs(packageManager); - await $({ - cwd: projectDir, - env: { - ...process.env, - ...getInstallEnvironment(packageManager), - }, - stderr: ["inherit", "pipe"], - })`${packageManager} ${installArgs}`; + const runInstall = async (args: string[]) => + $({ + cwd: projectDir, + env: { + ...process.env, + ...getInstallEnvironment(packageManager), + }, + stderr: ["inherit", "pipe"], + })`${packageManager} ${args}`; + + try { + await runInstall(installArgs); + } catch (error) { + const retryArgs = getInstallRetryArgs(packageManager, error); + if (!retryArgs) throw error; + await runInstall(retryArgs); + } s.stop("Dependencies installed successfully"); return { step, success: true }; diff --git a/apps/cli/test/generation/install-dependencies.test.ts b/apps/cli/test/generation/install-dependencies.test.ts index 0ce5fd44f..4f68d39ea 100644 --- a/apps/cli/test/generation/install-dependencies.test.ts +++ b/apps/cli/test/generation/install-dependencies.test.ts @@ -8,6 +8,7 @@ import { setupDatabase } from "@/helpers/core/db-setup"; import { getInstallArgs, getInstallEnvironment, + getInstallRetryArgs, installDependencies, } from "@/helpers/core/install-dependencies"; @@ -38,6 +39,22 @@ describe("getInstallArgs", () => { }); }); +describe("getInstallRetryArgs", () => { + it("refreshes Bun metadata after a registry propagation miss", () => { + const error = { + stderr: + 'error: No version matching "4.0.71" found for specifier "@ai-sdk/gateway" (but package exists)', + }; + + expect(getInstallRetryArgs("bun", error)).toEqual(["install", "--force"]); + expect(getInstallRetryArgs("npm", error)).toBeNull(); + }); + + it("does not retry unrelated Bun install failures", () => { + expect(getInstallRetryArgs("bun", new Error("network unavailable"))).toBeNull(); + }); +}); + describe("installDependencies", () => { it("returns a failure result instead of swallowing the error when install fails", async () => { // A non-existent cwd makes the install command fail fast (ENOENT) without