Skip to content

Commit b7d8340

Browse files
committed
Better DX for nested configs
1 parent 8d40918 commit b7d8340

4 files changed

Lines changed: 71 additions & 45 deletions

File tree

packages/app/aws-config/README.md

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,60 @@ import { getAwsConfig } from '@lokalise/aws-config';
1515
const awsConfig = getAwsConfig();
1616
```
1717

18-
#### Using with envase
18+
#### Using with envase (nested config)
1919

20-
For applications using the [envase](https://github.com/CatchMe2/envase) library for configuration management,
21-
use `getEnvaseAwsConfig()` to get schema and computed fragments that can be composed into your application's
22-
`createConfig()` call:
20+
For applications using [envase](https://github.com/CatchMe2/envase) that nest AWS config under a key like `aws`,
21+
pass `{ path: 'aws' }` so the computed resolvers access `fullParsedConfig.aws` instead of `fullParsedConfig`:
2322

2423
```ts
2524
import { createConfig, envvar } from 'envase';
2625
import { z } from 'zod';
2726
import { getEnvaseAwsConfig } from '@lokalise/aws-config';
2827

29-
const awsConfig = getEnvaseAwsConfig();
28+
const awsConfig = getEnvaseAwsConfig({ path: 'aws' });
3029

31-
// Spread fragments into your config (flat structure)
3230
const config = createConfig(process.env, {
3331
schema: {
34-
...awsConfig.schema,
32+
aws: awsConfig.schema,
3533
appName: envvar('APP_NAME', z.string()),
36-
port: envvar('PORT', z.coerce.number().default(3000)),
3734
},
3835
computed: {
39-
...awsConfig.computed,
36+
aws: awsConfig.computed,
4037
},
4138
});
4239

43-
// Access typed configuration
44-
console.log(config.region); // string
45-
console.log(config.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
40+
console.log(config.aws.region); // string
41+
console.log(config.aws.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
4642
console.log(config.appName); // string
4743
```
4844

49-
##### Nested AWS Namespace
45+
#### Using with envase (flat config)
5046

51-
When nesting AWS config under a namespace, wrap the computed resolver to access nested raw values:
47+
When AWS fields don't need to be namespaced, omit `path` and spread the fragments at root level:
5248

5349
```ts
50+
import { createConfig, envvar } from 'envase';
51+
import { z } from 'zod';
52+
import { getEnvaseAwsConfig } from '@lokalise/aws-config';
53+
5454
const awsConfig = getEnvaseAwsConfig();
5555

5656
const config = createConfig(process.env, {
5757
schema: {
58-
aws: awsConfig.schema,
58+
...awsConfig.schema,
5959
appName: envvar('APP_NAME', z.string()),
6060
},
6161
computed: {
62-
aws: {
63-
credentials: (raw: { aws: { accessKeyId?: string; secretAccessKey?: string } }) =>
64-
awsConfig.computed.credentials(raw.aws),
65-
},
62+
...awsConfig.computed,
6663
},
6764
});
6865

69-
console.log(config.aws.region); // string
70-
console.log(config.aws.credentials); // resolved credentials
66+
console.log(config.region); // string
67+
console.log(config.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
7168
```
7269

70+
#### Schema validation
71+
7372
The schema includes Zod validation with:
7473
- Required `region` field (must be non-empty string)
7574
- Optional `kmsKeyId` field (defaults to empty string)
@@ -78,7 +77,7 @@ The schema includes Zod validation with:
7877
- Optional `resourcePrefix` with max length validation (10 characters)
7978
- Optional `accessKeyId` and `secretAccessKey` fields for credential resolution
8079

81-
##### Credentials Resolution
80+
#### Credentials resolution
8281

8382
The `computed.credentials` resolver handles credential resolution:
8483
- If both `accessKeyId` and `secretAccessKey` are present: returns static credentials

packages/app/aws-config/src/envaseAwsConfig.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,27 +165,23 @@ describe('envaseAwsConfig', () => {
165165
testResetAwsConfig()
166166
})
167167

168-
it('works with nested aws namespace using wrapped computed', () => {
168+
it('works with nested aws namespace using path option', () => {
169169
const env = {
170170
AWS_REGION: DEFAULT_REGION,
171171
AWS_ACCESS_KEY_ID: 'access-key-id',
172172
AWS_SECRET_ACCESS_KEY: 'secret-access-key',
173173
APP_NAME: 'my-app',
174174
}
175175

176-
const awsConfig = getEnvaseAwsConfig()
176+
const awsConfig = getEnvaseAwsConfig({ path: 'aws' })
177177

178-
// When nesting, wrap the computed to access nested raw values
179178
const config = createConfig(env, {
180179
schema: {
181180
aws: awsConfig.schema,
182181
appName: envvar('APP_NAME', z.string()),
183182
},
184183
computed: {
185-
aws: {
186-
credentials: (raw: { aws: { accessKeyId?: string; secretAccessKey?: string } }) =>
187-
awsConfig.computed.credentials(raw.aws),
188-
},
184+
aws: awsConfig.computed,
189185
},
190186
})
191187

packages/app/aws-config/src/envaseAwsConfig.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,28 @@ export type EnvaseAwsConfigSchemaType = {
3333
/**
3434
* Type for the computed credentials resolver function.
3535
*/
36-
export type EnvaseAwsConfigComputedType = {
37-
credentials: (raw: {
38-
accessKeyId?: string
39-
secretAccessKey?: string
40-
}) => AwsCredentialIdentity | Provider<AwsCredentialIdentity>
36+
export type EnvaseAwsConfigComputedType<TRaw = { accessKeyId?: string; secretAccessKey?: string }> =
37+
{
38+
credentials: (raw: TRaw) => AwsCredentialIdentity | Provider<AwsCredentialIdentity>
39+
}
40+
41+
/**
42+
* Options for `getEnvaseAwsConfig()`.
43+
*/
44+
export type EnvaseAwsConfigOptions<TPath extends string | undefined = undefined> = {
45+
/** The key under which AWS schema is nested in your config. Omit for flat spread at root level. */
46+
path?: TPath
4147
}
4248

4349
/**
4450
* Return type of `getEnvaseAwsConfig()`.
4551
* Contains schema and computed fragments to spread into `createConfig()`.
4652
*/
47-
export type EnvaseAwsConfigFragments = {
53+
export type EnvaseAwsConfigFragments<TPath extends string | undefined = undefined> = {
4854
schema: EnvaseAwsConfigSchemaType
49-
computed: EnvaseAwsConfigComputedType
55+
computed: TPath extends string
56+
? EnvaseAwsConfigComputedType<Record<TPath, { accessKeyId?: string; secretAccessKey?: string }>>
57+
: EnvaseAwsConfigComputedType
5058
}
5159

5260
/**
@@ -96,19 +104,32 @@ const envaseAwsConfigSchema: EnvaseAwsConfigSchemaType = {
96104
}
97105

98106
/**
99-
* Computed values configuration for AWS config.
107+
* Creates computed values configuration for AWS config.
100108
* Derives `credentials` from the parsed `accessKeyId` and `secretAccessKey`.
101109
*/
102-
const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
103-
credentials: (raw: {
110+
function createAwsComputed<TPath extends string | undefined>(
111+
path?: TPath,
112+
): EnvaseAwsConfigFragments<TPath>['computed'] {
113+
const resolveCredentials = (awsRaw: {
104114
accessKeyId?: string
105115
secretAccessKey?: string
106116
}): AwsCredentialIdentity | Provider<AwsCredentialIdentity> => {
107-
if (raw.accessKeyId && raw.secretAccessKey) {
108-
return { accessKeyId: raw.accessKeyId, secretAccessKey: raw.secretAccessKey }
117+
if (awsRaw.accessKeyId && awsRaw.secretAccessKey) {
118+
return { accessKeyId: awsRaw.accessKeyId, secretAccessKey: awsRaw.secretAccessKey }
109119
}
110120
return createCredentialChain(fromTokenFile(), fromInstanceMetadata(), fromEnv(), fromIni())
111-
},
121+
}
122+
123+
if (path) {
124+
return {
125+
// biome-ignore lint/suspicious/noExplicitAny: raw config shape depends on consumer's schema
126+
credentials: (raw: any) => resolveCredentials(raw[path]),
127+
} as EnvaseAwsConfigFragments<TPath>['computed']
128+
}
129+
130+
return {
131+
credentials: resolveCredentials,
132+
} as EnvaseAwsConfigFragments<TPath>['computed']
112133
}
113134

114135
/**
@@ -118,13 +139,17 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
118139
* your application's configuration. This allows composing AWS config with other
119140
* application-specific configuration.
120141
*
142+
* Use the `path` option when nesting AWS config under a key (e.g., `aws`),
143+
* so that computed resolvers access `fullParsedConfig.aws` instead of `fullParsedConfig`.
144+
*
121145
* @example
122146
* ```typescript
123147
* import { createConfig, envvar } from 'envase'
124148
* import { z } from 'zod'
125149
* import { getEnvaseAwsConfig } from '@lokalise/aws-config'
126150
*
127-
* const awsConfig = getEnvaseAwsConfig()
151+
* // Nested under 'aws' key (recommended):
152+
* const awsConfig = getEnvaseAwsConfig({ path: 'aws' })
128153
*
129154
* const config = createConfig(process.env, {
130155
* schema: {
@@ -137,6 +162,8 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
137162
* })
138163
*
139164
* // Or spread directly at root level:
165+
* const awsConfig = getEnvaseAwsConfig()
166+
*
140167
* const config = createConfig(process.env, {
141168
* schema: {
142169
* ...awsConfig.schema,
@@ -148,11 +175,14 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
148175
* })
149176
* ```
150177
*
178+
* @param options - Optional configuration. Use `path` to specify the nesting key.
151179
* @returns Schema and computed fragments for AWS configuration
152180
*/
153-
export const getEnvaseAwsConfig = (): EnvaseAwsConfigFragments => {
181+
export function getEnvaseAwsConfig<TPath extends string | undefined = undefined>(
182+
options?: EnvaseAwsConfigOptions<TPath>,
183+
): EnvaseAwsConfigFragments<TPath> {
154184
return {
155185
schema: envaseAwsConfigSchema,
156-
computed: envaseAwsConfigComputed,
186+
computed: createAwsComputed(options?.path),
157187
}
158188
}

packages/app/aws-config/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { AWS_CONFIG_ENV_VARS, type AwsConfig, getAwsConfig } from './awsConfig.t
33
export {
44
type EnvaseAwsConfigComputedType,
55
type EnvaseAwsConfigFragments,
6+
type EnvaseAwsConfigOptions,
67
type EnvaseAwsConfigSchemaType,
78
getEnvaseAwsConfig,
89
} from './envaseAwsConfig.ts'

0 commit comments

Comments
 (0)