-
-
Notifications
You must be signed in to change notification settings - Fork 95
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
Expose an option to customise schema combination #313
base: main
Are you sure you want to change the base?
Changes from 9 commits
a88f593
b96a966
568ecbc
5c5881e
c1cc652
1c236cc
08d2223
6705f40
6127da7
e8c634b
e92c15b
4d7def6
df61ee8
26532c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--- | ||
"@t3-oss/env-nextjs": minor | ||
"@t3-oss/env-core": minor | ||
"@t3-oss/env-nuxt": minor | ||
--- | ||
|
||
feat!: added ability to customise schema combination | ||
|
||
Combination of schemas can now be customised using the `createFinalSchema` option. This allows further refinement or transformation of the environment variables. | ||
|
||
This comes with a type-only breaking change: | ||
|
||
- `CreateEnv` now has the signature `CreateEnv<TFinalSchema, TExtends>`, instead of the previous `CreateEnv<TServer, TClient, TShared, TExtends>`. | ||
- Previous behaviour can be achieved by using `DefaultCombinedSchema<TServer, TClient, TShared>` as the type for `TFinalSchema`. | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -629,3 +629,115 @@ describe("extending presets", () => { | |
}); | ||
}); | ||
}); | ||
|
||
describe("createFinalSchema", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how does this work with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can do - extends just assigns all of the properties to the final result from the schema, like it did before There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what I was thinking of was more like what if you do some combinator that accesses server stuff, will that cause an "invalid server access" or not? I'd think not since you're not giving the proxy as the argument there right? also what are the types in that callback like, server variables will be undefined when the combinator runs on the client etc etc There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see #240, the whole Proxy thing doesn't really work properly with As for types, you're correct - it's currently typed to what it'd be on the server, like the rest of the library (client + server + shared). I don't know how much it would complicate things to mark the server variables as optional. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No since accessing a server var on the client would throw an error - so e.g if you'd chain on a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. schemas have never been run on the output of presets, they were always run before it. they have always been passed the raw runtimeEnv option, and any presets were merged into the parsed result. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the concern, but I don't think there's a good way to expose type-wise that the server variables might not be defined to the We could possibly pass some sort of createObjectSchema: (shape, { isServer, makeSafe }) => z.object(shape).refine((_env) => {
const env = makeSafe(_env) // wraps in Proxy
if (!isServer) env.SERVER_ENV // throws
// ...
}) |
||
test("custom schema combiner", () => { | ||
let receivedIsServer = false; | ||
const env = createEnv({ | ||
server: { | ||
SERVER_ENV: v.string(), | ||
}, | ||
shared: { | ||
SHARED_ENV: v.string(), | ||
}, | ||
clientPrefix: "CLIENT_", | ||
client: { | ||
CLIENT_ENV: v.string(), | ||
}, | ||
runtimeEnv: { | ||
SERVER_ENV: "server", | ||
SHARED_ENV: "shared", | ||
CLIENT_ENV: "client", | ||
}, | ||
createFinalSchema: (shape, isServer) => { | ||
expectTypeOf(isServer).toEqualTypeOf<boolean>(); | ||
if (typeof isServer === "boolean") receivedIsServer = true; | ||
juliusmarminge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return v.object(shape); | ||
}, | ||
}); | ||
expectTypeOf(env).toEqualTypeOf< | ||
Readonly<{ | ||
SERVER_ENV: string; | ||
SHARED_ENV: string; | ||
CLIENT_ENV: string; | ||
}> | ||
>(); | ||
expect(env).toMatchObject({ | ||
SERVER_ENV: "server", | ||
SHARED_ENV: "shared", | ||
CLIENT_ENV: "client", | ||
}); | ||
expect(receivedIsServer).toBe(true); | ||
}); | ||
test("schema combiner with further refinement", () => { | ||
const env = createEnv({ | ||
server: { | ||
SKIP_AUTH: v.optional(v.boolean()), | ||
EMAIL: v.optional(v.pipe(v.string(), v.email())), | ||
PASSWORD: v.optional(v.pipe(v.string(), v.minLength(1))), | ||
}, | ||
runtimeEnv: { | ||
SKIP_AUTH: true, | ||
}, | ||
createFinalSchema: (shape) => | ||
v.pipe( | ||
v.object(shape), | ||
v.check((env) => env.SKIP_AUTH || !!(env.EMAIL && env.PASSWORD)), | ||
), | ||
}); | ||
expectTypeOf(env).toEqualTypeOf< | ||
Readonly<{ | ||
SKIP_AUTH?: boolean; | ||
EMAIL?: string; | ||
PASSWORD?: string; | ||
}> | ||
>(); | ||
expect(env).toMatchObject({ SKIP_AUTH: true }); | ||
}); | ||
test("schema combiner that changes the type", () => { | ||
const env = createEnv({ | ||
server: { | ||
SKIP_AUTH: v.optional(v.boolean()), | ||
EMAIL: v.optional(v.pipe(v.string(), v.email())), | ||
PASSWORD: v.optional(v.pipe(v.string(), v.minLength(1))), | ||
}, | ||
runtimeEnv: { | ||
SKIP_AUTH: true, | ||
}, | ||
createFinalSchema: (shape) => | ||
v.pipe( | ||
v.object(shape), | ||
v.rawTransform(({ addIssue, dataset, NEVER }) => { | ||
const env = dataset.value; | ||
if (env.SKIP_AUTH) return { SKIP_AUTH: true } as const; | ||
if (!env.EMAIL || !env.PASSWORD) { | ||
addIssue({ | ||
message: | ||
"EMAIL and PASSWORD are required if SKIP_AUTH is false", | ||
}); | ||
return NEVER; | ||
} | ||
return { | ||
EMAIL: env.EMAIL, | ||
PASSWORD: env.PASSWORD, | ||
}; | ||
}), | ||
), | ||
}); | ||
expectTypeOf(env).toEqualTypeOf< | ||
Readonly< | ||
| { | ||
readonly SKIP_AUTH: true; | ||
EMAIL?: undefined; | ||
PASSWORD?: undefined; | ||
} | ||
| { | ||
readonly SKIP_AUTH?: undefined; | ||
EMAIL: string; | ||
PASSWORD: string; | ||
} | ||
> | ||
>(); | ||
expect(env).toMatchObject({ SKIP_AUTH: true }); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a user-breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateEnv is exported, so technically yes - in practicality I doubt anyone was relying on it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's exported cause the other packages needs it. I should add some @internal jsdoc annotations to things