Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable users to define their env var validations #2363

Open
wants to merge 19 commits into
base: miho-zod-env
Choose a base branch
from

Conversation

infomiho
Copy link
Contributor

@infomiho infomiho commented Oct 30, 2024

The idea

Wasp uses Zod to define its validation rules for the env variables it expects e.g. it defines that JWT_SECRET is required and a string. After this validation passes, Zod gives us a validated object that we can safely use knowing that e.g. JWT_SECRET is present and it is a string:

import { env } from 'wasp/server/env'

const jwtSecret: string = env.JWT_SECRET

Users need their own user-land env vars e.g. STRIPE_API_KEY. Some users validate their env vars with manual checks to make sure that the env var is set before running the app e.g.

if (!process.env.STRIPE_API_KEY) {
  throw new Error('STRIPE_API_KEY is missing');
}

Since Wasp already uses Zod to validate env vars, we want to expose the same mechanism to our users. There are two main benefits:

  1. Users don't have to come up with their own validation mechanisms
  2. Users can use the same env object for both Wasp defined env vars and their own user-land env vars

How this works

Users need to define their env vars validation by defining a Zod object schema. They then declare that in the Wasp file. Something like this:

// env.ts
import * as z from 'zod'
import type { EnvValidationFn } from 'wasp/env'

export const serverEnvValidationFn = (() =>
  z.object({
    STRIPE_API_KEY: z.string({
      required_error: 'STRIPE_API_KEY is required.',
    }),
  })) satisfies EnvValidationFn

export const clientEnvValidationFn = (() =>
  z.object({
    REACT_APP_NAME: z.string().default('TODO App'),
  })) satisfies EnvValidationFn

and then:

server: {
  ...
  envValidationFn: import { serverEnvValidationFn } from "@src/env",
},
client: {
  ...
  envValidationFn: import { clientEnvValidationFn } from "@src/env",
},

Wasp merges this user defined schema with its built-in env vars validation schema when it validates the process.env object on the server and the import.meta.env object on the client.

Now users can use the env object to access their env vars like this:

import { env } from 'wasp/server/env'

const stripeApiKey: string = env.STRIPE_API_KEY

Caveats

  • users will need to be careful not to cause circular imports. Ideally they use a file only for env schemas e.g. env.ts
  • user will need to use the EnvValidationFn with satisfies
  • maybe this will clash with Build time validation for client env #2392 (there will be conflicts that we'll resolve)

@infomiho infomiho mentioned this pull request Nov 26, 2024
5 tasks
@infomiho infomiho marked this pull request as ready for review December 18, 2024 09:34
@Martinsos
Copy link
Member

Caveats:

  • users will need to be careful not to cause circular imports. Ideally they use a file only for env schemas e.g. env.ts
  • user will need to use the EnvValidationFn with satisfies
  • maybe this will clash with Build time validation for client env #2392

@infomiho could you please describe how this works, what is the experience for the user? I see there are no docs yet, and it would help me with reviewing so I don't have to guess it from the code + so I can consider if code does indeed do what the idea was.

@infomiho
Copy link
Contributor Author

infomiho commented Jan 3, 2025

@Martinsos I've updated the PR description to give you a proper intro into the task I was tackling

@Martinsos
Copy link
Member

@infomiho from the high level / DX perspective, sounds good to me, thanks for the detailed description! If they define the same var as Wasp already defines, what happens? Conflict or override or nothing? What do we want to happen, is there any benefit in them being able to override Wasp's var definitions? Maybe conflict is the best?

@infomiho
Copy link
Contributor Author

infomiho commented Jan 8, 2025

@Martinsos right now, since we merge their schema into ours, they can override our rules: example how that works

If their validation rules are less strict or wrong, they will get a type error (change JWT_SECRET to number) or even a runtime error (make something optional and then it fails in the runtime).

We can merge our schema into theirs, therefore our rules would always win - that seems okay to me, even good enough.

I think, we could go through their schema keys and our schema keys and if they are conflicts, output a custom error. But this is something I would need to check.

@infomiho infomiho requested a review from sodic January 8, 2025 12:10
Copy link
Contributor

@sodic sodic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Two main requests: the public API change and updating the TS SDK.

What was the naming problem you mentioned?

@@ -1,25 +1,7 @@
import * as z from 'zod'

const redColor = '\x1b[31m'
// PUBLIC API
export type EnvValidationFn = () => z.ZodObject<any>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table pls if not yet 😬

const serverCommonSchema = z.object({
{=# envValidationFn.isDefined =}
{=& envValidationFn.importStatement =}
const userServerEnvSchema = {= envValidationFn.importIdentifier =}()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this in person. The verdict was we'll make the API similar to userSignupFields

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"type": "module", was added in the package.json

I guess somebody added it when testing the Wasp TS config 🤷

@@ -16,6 +16,7 @@ data Client = Client
{ setupFn :: Maybe ExtImport,
rootComponent :: Maybe ExtImport,
-- We expect the base dir to start with a slash e.g. /client
baseDir :: Maybe String
baseDir :: Maybe String,
envValidationFn :: Maybe ExtImport
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to update the TS SDK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants