From b645af1337799d7b035c90a41e300f55331d2b09 Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Tue, 3 Dec 2024 12:01:27 -0500 Subject: [PATCH] add zod validated configuration and context --- .env | 2 ++ .gitignore | 2 -- README.md | 1 + bun.lockb | Bin 29492 -> 29827 bytes package.json | 3 ++- src/config/index.ts | 22 ++++++++++++++++++++++ src/prisma/index.ts | 3 +++ src/yoga/builder.ts | 29 +++++++++++++++++++++++++++++ src/yoga/context.ts | 10 ++++++++++ src/yoga/index.ts | 2 ++ src/yoga/schema.ts | 29 +++++------------------------ 11 files changed, 76 insertions(+), 27 deletions(-) create mode 100644 .env create mode 100644 src/config/index.ts create mode 100644 src/prisma/index.ts create mode 100644 src/yoga/builder.ts create mode 100644 src/yoga/context.ts diff --git a/.env b/.env new file mode 100644 index 0000000..c98868d --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +APP_VERSION=1.0.0-alpha +DATABASE_URL="file:./dev.db" diff --git a/.gitignore b/.gitignore index 11ddd8d..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ node_modules -# Keep environment variables out of version control -.env diff --git a/README.md b/README.md index ae6cb47..294f769 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,4 @@ bun run src/index.ts - **Pothos** - **Prisma** Database ORM - **Pino** Logger +- **Zod** Schema validation diff --git a/bun.lockb b/bun.lockb index 6570e5bb6efff32eaabb4a7685a13762b76e507b..196c00f2d35624b8453e9862aa71450d8863ba4d 100755 GIT binary patch delta 4833 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 Jix($H{skJvSjzwa diff --git a/package.json b/package.json index 5a89fa5..495f3ef 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "graphql": "^16.9.0", "graphql-yoga": "^5.10.4", "pino": "^9.5.0", - "pino-pretty": "^13.0.0" + "pino-pretty": "^13.0.0", + "zod": "^3.23.8" } } diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..f4c0bc5 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,22 @@ +import { logger } from "@lib/logger"; +import { z } from "zod"; + +export interface Configuration { + app_version: string; +} + +export const LoadConfig = (): Configuration => { + const { success, data, error } = z + .object({ + APP_VERSION: z.string().default("development"), + }) + .safeParse(process.env); + + if (!success) { + logger.error(error.message); + } + + return { + app_version: data!.APP_VERSION, + }; +}; diff --git a/src/prisma/index.ts b/src/prisma/index.ts new file mode 100644 index 0000000..901f3a0 --- /dev/null +++ b/src/prisma/index.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from "@prisma/client"; + +export const prisma = new PrismaClient(); diff --git a/src/yoga/builder.ts b/src/yoga/builder.ts new file mode 100644 index 0000000..565bdc3 --- /dev/null +++ b/src/yoga/builder.ts @@ -0,0 +1,29 @@ +import type { Configuration } from "@app/config"; +import { prisma } from "@app/prisma"; +import SchemaBuilder from "@pothos/core"; +import PrismaPlugin, { + type PrismaTypesFromClient, +} from "@pothos/plugin-prisma"; +import type { YogaInitialContext } from "graphql-yoga"; + +type Context = YogaInitialContext & { + config: Configuration; +}; + +export const builder = new SchemaBuilder<{ + Context: Context; + PrismaTypes: PrismaTypesFromClient; +}>({ + plugins: [PrismaPlugin], + prisma: { + client: prisma, + // defaults to false, uses /// comments from prisma schema as descriptions + // for object types, relations and exposed fields. + // descriptions can be omitted by setting description to false + exposeDescriptions: false, + // use where clause from prismaRelatedConnection for totalCount (defaults to true) + filterConnectionTotalCount: true, + // warn when not using a query parameter correctly + onUnusedQuery: process.env.NODE_ENV === "production" ? null : "warn", + }, +}); diff --git a/src/yoga/context.ts b/src/yoga/context.ts new file mode 100644 index 0000000..bc85aa7 --- /dev/null +++ b/src/yoga/context.ts @@ -0,0 +1,10 @@ +import { LoadConfig } from "@app/config"; +import type { YogaInitialContext } from "graphql-yoga"; + +export const context = (initialContext: YogaInitialContext) => { + const config = LoadConfig(); + return { + ...initialContext, + config, + }; +}; diff --git a/src/yoga/index.ts b/src/yoga/index.ts index 43ce842..8d113ba 100644 --- a/src/yoga/index.ts +++ b/src/yoga/index.ts @@ -1,8 +1,10 @@ import { yogaLogger } from "@lib/logger"; import { createYoga } from "graphql-yoga"; +import { context } from "./context"; import { schema } from "./schema"; export const yoga = createYoga({ schema, + context: context, logging: yogaLogger, }); diff --git a/src/yoga/schema.ts b/src/yoga/schema.ts index fad1003..f72cc19 100644 --- a/src/yoga/schema.ts +++ b/src/yoga/schema.ts @@ -1,27 +1,5 @@ -import SchemaBuilder from "@pothos/core"; -import PrismaPlugin, { - type PrismaTypesFromClient, -} from "@pothos/plugin-prisma"; -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -const builder = new SchemaBuilder<{ - PrismaTypes: PrismaTypesFromClient; -}>({ - plugins: [PrismaPlugin], - prisma: { - client: prisma, - // defaults to false, uses /// comments from prisma schema as descriptions - // for object types, relations and exposed fields. - // descriptions can be omitted by setting description to false - exposeDescriptions: false, - // use where clause from prismaRelatedConnection for totalCount (defaults to true) - filterConnectionTotalCount: true, - // warn when not using a query parameter correctly - onUnusedQuery: process.env.NODE_ENV === "production" ? null : "warn", - }, -}); +import { prisma } from "@app/prisma"; +import { builder } from "./builder"; const User = builder.prismaObject("User", { fields: (t) => ({ @@ -44,6 +22,9 @@ const Post = builder.prismaObject("Post", { builder.queryType({ fields: (t) => ({ + version: t.string({ + resolve: (parent, args, context) => context.config.app_version, + }), users: t.prismaField({ type: [User], resolve: async () => {