Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions packages/app/aws-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,60 @@ import { getAwsConfig } from '@lokalise/aws-config';
const awsConfig = getAwsConfig();
```

#### Using with envase
#### Using with envase (nested config)

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

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

const awsConfig = getEnvaseAwsConfig();
const awsConfig = getEnvaseAwsConfig({ path: 'aws' });

// Spread fragments into your config (flat structure)
const config = createConfig(process.env, {
schema: {
...awsConfig.schema,
aws: awsConfig.schema,
appName: envvar('APP_NAME', z.string()),
port: envvar('PORT', z.coerce.number().default(3000)),
},
computed: {
...awsConfig.computed,
aws: awsConfig.computed,
},
});

// Access typed configuration
console.log(config.region); // string
console.log(config.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
console.log(config.aws.region); // string
console.log(config.aws.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
console.log(config.appName); // string
```

##### Nested AWS Namespace
#### Using with envase (flat config)

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

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

const awsConfig = getEnvaseAwsConfig();

const config = createConfig(process.env, {
schema: {
aws: awsConfig.schema,
...awsConfig.schema,
appName: envvar('APP_NAME', z.string()),
},
computed: {
aws: {
credentials: (raw: { aws: { accessKeyId?: string; secretAccessKey?: string } }) =>
awsConfig.computed.credentials(raw.aws),
},
...awsConfig.computed,
},
});

console.log(config.aws.region); // string
console.log(config.aws.credentials); // resolved credentials
console.log(config.region); // string
console.log(config.credentials); // AwsCredentialIdentity | Provider<AwsCredentialIdentity>
```

#### Schema validation

The schema includes Zod validation with:
- Required `region` field (must be non-empty string)
- Optional `kmsKeyId` field (defaults to empty string)
Expand All @@ -78,7 +77,7 @@ The schema includes Zod validation with:
- Optional `resourcePrefix` with max length validation (10 characters)
- Optional `accessKeyId` and `secretAccessKey` fields for credential resolution

##### Credentials Resolution
#### Credentials resolution

The `computed.credentials` resolver handles credential resolution:
- If both `accessKeyId` and `secretAccessKey` are present: returns static credentials
Expand Down
10 changes: 3 additions & 7 deletions packages/app/aws-config/src/envaseAwsConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,23 @@ describe('envaseAwsConfig', () => {
testResetAwsConfig()
})

it('works with nested aws namespace using wrapped computed', () => {
it('works with nested aws namespace using path option', () => {
const env = {
AWS_REGION: DEFAULT_REGION,
AWS_ACCESS_KEY_ID: 'access-key-id',
AWS_SECRET_ACCESS_KEY: 'secret-access-key',
APP_NAME: 'my-app',
}

const awsConfig = getEnvaseAwsConfig()
const awsConfig = getEnvaseAwsConfig({ path: 'aws' })

// When nesting, wrap the computed to access nested raw values
const config = createConfig(env, {
schema: {
aws: awsConfig.schema,
appName: envvar('APP_NAME', z.string()),
},
computed: {
aws: {
credentials: (raw: { aws: { accessKeyId?: string; secretAccessKey?: string } }) =>
awsConfig.computed.credentials(raw.aws),
},
aws: awsConfig.computed,
},
})

Expand Down
62 changes: 46 additions & 16 deletions packages/app/aws-config/src/envaseAwsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,28 @@ export type EnvaseAwsConfigSchemaType = {
/**
* Type for the computed credentials resolver function.
*/
export type EnvaseAwsConfigComputedType = {
credentials: (raw: {
accessKeyId?: string
secretAccessKey?: string
}) => AwsCredentialIdentity | Provider<AwsCredentialIdentity>
export type EnvaseAwsConfigComputedType<TRaw = { accessKeyId?: string; secretAccessKey?: string }> =
{
credentials: (raw: TRaw) => AwsCredentialIdentity | Provider<AwsCredentialIdentity>
}

/**
* Options for `getEnvaseAwsConfig()`.
*/
export type EnvaseAwsConfigOptions<TPath extends string | undefined = undefined> = {
/** The key under which AWS schema is nested in your config. Omit for flat spread at root level. */
path?: TPath
}

/**
* Return type of `getEnvaseAwsConfig()`.
* Contains schema and computed fragments to spread into `createConfig()`.
*/
export type EnvaseAwsConfigFragments = {
export type EnvaseAwsConfigFragments<TPath extends string | undefined = undefined> = {
schema: EnvaseAwsConfigSchemaType
computed: EnvaseAwsConfigComputedType
computed: TPath extends string
? EnvaseAwsConfigComputedType<Record<TPath, { accessKeyId?: string; secretAccessKey?: string }>>
: EnvaseAwsConfigComputedType
}

/**
Expand Down Expand Up @@ -96,19 +104,32 @@ const envaseAwsConfigSchema: EnvaseAwsConfigSchemaType = {
}

/**
* Computed values configuration for AWS config.
* Creates computed values configuration for AWS config.
* Derives `credentials` from the parsed `accessKeyId` and `secretAccessKey`.
*/
const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
credentials: (raw: {
function createAwsComputed<TPath extends string | undefined>(
path?: TPath,
): EnvaseAwsConfigFragments<TPath>['computed'] {
const resolveCredentials = (awsRaw: {
accessKeyId?: string
secretAccessKey?: string
}): AwsCredentialIdentity | Provider<AwsCredentialIdentity> => {
if (raw.accessKeyId && raw.secretAccessKey) {
return { accessKeyId: raw.accessKeyId, secretAccessKey: raw.secretAccessKey }
if (awsRaw.accessKeyId && awsRaw.secretAccessKey) {
return { accessKeyId: awsRaw.accessKeyId, secretAccessKey: awsRaw.secretAccessKey }
}
return createCredentialChain(fromTokenFile(), fromInstanceMetadata(), fromEnv(), fromIni())
},
}

if (path) {
return {
// biome-ignore lint/suspicious/noExplicitAny: raw config shape depends on consumer's schema
credentials: (raw: any) => resolveCredentials(raw[path]),
} as EnvaseAwsConfigFragments<TPath>['computed']
}

return {
credentials: resolveCredentials,
} as EnvaseAwsConfigFragments<TPath>['computed']
}

/**
Expand All @@ -118,13 +139,17 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
* your application's configuration. This allows composing AWS config with other
* application-specific configuration.
*
* Use the `path` option when nesting AWS config under a key (e.g., `aws`),
* so that computed resolvers access `fullParsedConfig.aws` instead of `fullParsedConfig`.
*
* @example
* ```typescript
* import { createConfig, envvar } from 'envase'
* import { z } from 'zod'
* import { getEnvaseAwsConfig } from '@lokalise/aws-config'
*
* const awsConfig = getEnvaseAwsConfig()
* // Nested under 'aws' key (recommended):
* const awsConfig = getEnvaseAwsConfig({ path: 'aws' })
*
* const config = createConfig(process.env, {
* schema: {
Expand All @@ -137,6 +162,8 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
* })
*
* // Or spread directly at root level:
* const awsConfig = getEnvaseAwsConfig()
*
* const config = createConfig(process.env, {
* schema: {
* ...awsConfig.schema,
Expand All @@ -148,11 +175,14 @@ const envaseAwsConfigComputed: EnvaseAwsConfigComputedType = {
* })
* ```
*
* @param options - Optional configuration. Use `path` to specify the nesting key.
* @returns Schema and computed fragments for AWS configuration
*/
export const getEnvaseAwsConfig = (): EnvaseAwsConfigFragments => {
export function getEnvaseAwsConfig<TPath extends string | undefined = undefined>(
options?: EnvaseAwsConfigOptions<TPath>,
): EnvaseAwsConfigFragments<TPath> {
return {
schema: envaseAwsConfigSchema,
computed: envaseAwsConfigComputed,
computed: createAwsComputed(options?.path),
}
}
1 change: 1 addition & 0 deletions packages/app/aws-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { AWS_CONFIG_ENV_VARS, type AwsConfig, getAwsConfig } from './awsConfig.t
export {
type EnvaseAwsConfigComputedType,
type EnvaseAwsConfigFragments,
type EnvaseAwsConfigOptions,
type EnvaseAwsConfigSchemaType,
getEnvaseAwsConfig,
} from './envaseAwsConfig.ts'
Expand Down
Loading