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 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 a3a217f57..15a72346d 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", @@ -633,6 +649,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 +791,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", @@ -1101,6 +1121,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 +1181,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 +1241,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 +1301,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 +1363,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 +1425,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 +1485,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 +1545,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 +1644,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 +1769,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 +2052,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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

+ +
+ +
); } @@ -2273,6 +2409,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 +2429,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 +2589,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -2441,6 +2598,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 +2688,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 +3107,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 +3127,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 +3472,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -3293,6 +3481,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 +3572,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 +3978,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -3790,6 +3987,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 +4076,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 +4521,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -4324,6 +4530,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 +4621,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 +5384,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -5178,6 +5393,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 +5767,47 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { trpc } from "@/utils/trpc"; import { useQuery } from "@tanstack/react-query"; +import { trpc } from "@/utils/trpc"; + 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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -6022,7 +6240,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 +6563,47 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { orpc } from "@/utils/orpc"; import { useQuery } from "@tanstack/react-query"; +import { orpc } from "@/utils/orpc"; + 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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -6754,6 +6967,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -6762,6 +6976,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 +7066,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 +7424,14 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { env } from "@snapshot-hono-openapi/env/web"; import { useEffect, useState } from "react"; +import { env } from "@snapshot-hono-openapi/env/web"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const [healthCheck, setHealthCheck] = useState({ isLoading: true, data: false }); @@ -7242,25 +7450,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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -7904,6 +8121,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -7912,6 +8130,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 +8220,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 +8522,14 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { graphqlFetch } from "@/utils/graphql"; import { useQuery } from "@tanstack/react-query"; +import { graphqlFetch } from "@/utils/graphql"; + export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; function HomeComponent() { const healthCheck = useQuery({ @@ -8326,25 +8538,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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -8996,6 +9217,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -9004,6 +9226,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 +9316,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 +9626,180 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { useState } from "react"; + +import { authClient } from "@/lib/auth-client"; +import { trpc, trpcClient } from "@/utils/trpc"; 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 userId = session?.user.id; - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + // 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; -function HomeComponent() { - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + const tasks = useQuery({ + queryKey: tasksQueryKey, + queryFn: () => trpcClient.tasks.list.query(), + enabled: Boolean(userId), + }); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; + + if (sessionPending) { + return

Checking your session...

; + } + + if (!userId) { + 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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +10040,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 +10060,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 +10411,7 @@ export const accountRelations = relations(account, ({ one }) => ({ { "content": "export * from "./auth"; +export * from "./task"; // @@ -10039,6 +10420,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 +10696,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. +

+
+ +
+ + + {healthCheck === undefined + ? "Checking API..." + : healthCheck === "OK" + ? "API connected" + : "API error"} + +
); } @@ -12266,52 +12650,47 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { trpc } from "@/utils/trpc"; import { useQuery } from "@tanstack/react-query"; +import { trpc } from "@/utils/trpc"; + 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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -13005,52 +13384,47 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; -import { trpc } from "@/utils/trpc"; import { useQuery } from "@tanstack/react-query"; +import { trpc } from "@/utils/trpc"; + 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. +

+
+ +
+ + + {healthCheck.isLoading + ? "Checking API..." + : healthCheck.data + ? "API connected" + : "API disconnected"} + +
); } @@ -13483,7 +13857,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 +14145,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +14502,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 +14522,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 +14682,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -14198,6 +14691,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 +14781,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 +15069,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +15426,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 +15446,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 +15606,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -14995,6 +15615,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 +15705,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 +16095,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 +16115,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 +16275,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -15635,6 +16284,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 +16375,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 +16671,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +17034,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 +17054,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 +17214,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -16447,6 +17223,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 +17313,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 +17600,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +17963,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 +17983,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 +18143,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -17249,6 +18152,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 +18242,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 +18525,150 @@ if (!rootElement.innerHTML) { { "content": "import { createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + import { trpc } from "@/utils/trpc"; -import { useQuery } from "@tanstack/react-query"; export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = \` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function TaskPanel() { + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + const tasksQueryKey = trpc.tasks.list.queryKey(); - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - \`; + const tasks = useQuery(trpc.tasks.list.queryOptions()); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; -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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +18887,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 +18907,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 +19067,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -18046,6 +19076,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 +19333,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. +

+
+ +
+
); } @@ -18533,7 +19567,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 +20143,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 +20163,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 +20323,7 @@ export const db = drizzle({ client, schema }); { "content": "export * from "./example"; +export * from "./task"; // @@ -19277,6 +20332,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..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,6 +29,16 @@ import { {{#if (isBetterAuth auth)}}protectedProcedure, {{/if}}publicProcedure, router, } from "../index{{#if (eq backend "adonisjs")}}.js{{/if}}"; +{{#if (and (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} +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 (and (eq orm "drizzle") (or (isBetterAuth auth) (eq auth "none")))}} + 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..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,7 +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{{#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 new file mode 100644 index 000000000..74cd7d07d --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/base/src/tasks.ts.hbs @@ -0,0 +1,65 @@ +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +import { {{#if (isBetterAuth auth)}}and, {{/if}}desc, eq } from "drizzle-orm"; + +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; + +{{#if (isBetterAuth auth)}} +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}} +{{/if}} 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 new file mode 100644 index 000000000..c45fd4755 --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/mysql/src/schema/task.ts.hbs @@ -0,0 +1,29 @@ +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +{{#if (isBetterAuth auth)}} +import { boolean, index, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core"; + +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; +{{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 (isBetterAuth auth)}} + userId: varchar("user_id", { length: 36 }) + .notNull() + .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 (isBetterAuth auth)}}, + (table) => [index("task_user_id_idx").on(table.userId)]{{/if}} +); +{{/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 new file mode 100644 index 000000000..fd805f0c6 --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/postgres/src/schema/task.ts.hbs @@ -0,0 +1,29 @@ +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +{{#if (isBetterAuth auth)}} +import { boolean, index, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; + +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; +{{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 (isBetterAuth auth)}} + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + {{/if}} + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }{{#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/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 new file mode 100644 index 000000000..185d514d6 --- /dev/null +++ b/packages/template-generator/templates/db/drizzle/sqlite/src/schema/task.ts.hbs @@ -0,0 +1,32 @@ +{{#if (or (isBetterAuth auth) (eq auth "none"))}} +import { sql } from "drizzle-orm"; +{{#if (isBetterAuth auth)}} +import { sqliteTable, integer, text, index } from "drizzle-orm/sqlite-core"; + +import { user } from "./auth{{#if (eq backend "adonisjs")}}.js{{/if}}"; +{{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 (isBetterAuth auth)}} + userId: text("user_id") + .notNull() + .references(() => user.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 (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 4e3362e02..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 @@ -1,45 +1,195 @@ -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 { api } from "@{{projectName}}/backend/convex/_generated/api"; +{{else}} +import { createFileRoute } from "@tanstack/react-router"; +{{#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 (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 { useState } from "react"; + +{{#if (isBetterAuth auth)}} +import { authClient } from "@/lib/auth-client"; +{{/if}} +import { trpc{{#if (isBetterAuth auth)}}, trpcClient{{/if}} } 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 { query, resolved } from "@/utils/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 { graphqlFetch } from "@/utils/graphql"; import { useQuery } from "@tanstack/react-query"; + +import { graphqlFetch } from "@/utils/graphql"; {{else if (eq api "openapi")}} -import { env } from "@{{projectName}}/env/web"; import { useEffect, useState } from "react"; + +import { env } from "@{{projectName}}/env/web"; +{{/if}} {{/if}} export const Route = createFileRoute("/")({ component: HomeComponent, }); -const TITLE_TEXT = ` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ - - ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - █████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - `; +{{#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; + + // 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({ + queryKey: tasksQueryKey, + queryFn: () => trpcClient.tasks.list.query(), + enabled: Boolean(userId), + }); + {{else}} + const tasksQueryKey = trpc.tasks.list.queryKey(); + + const tasks = useQuery(trpc.tasks.list.queryOptions()); + {{/if}} + + const invalidate = () => queryClient.invalidateQueries({ queryKey: tasksQueryKey }); + + 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, + }); + + const mutationError = create.error ?? setCompleted.error ?? remove.error; + + {{#if (isBetterAuth auth)}} + if (sessionPending) { + return

Checking your session...

; + } + + if (!userId) { + 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} + /> + +
+ + {mutationError ? ( +

+ {mutationError.message} +

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

Loading tasks...

+ ) : tasks.isError ? ( +

+ Could not load tasks: {tasks.error.message} +

+ ) : 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 +228,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}}{{#unless (eq orm "none")}} through {{orm}}{{/unless}}{{/unless}}{{#if (isBetterAuth auth)}}, Better Auth{{/if}}. +

+
+ +
+

Tasks

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

+ This stack has no database-backed task API yet. Replace this panel with your first + feature. +

+ {{/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([