0.6.11 • Published 9 months ago

@clipboard-health/config v0.6.11

Weekly downloads
-
License
MIT
Repository
-
Last release
9 months ago

@clipboard-health/config

Type-safe static configuration management: a pure function to resolve, validate against a Zod schema, and freeze configuration values.

Table of contents

Install

npm install @clipboard-health/config

Usage

Type-safe configuration

The TypeDoc comment for the createConfig function:

// ./src/lib/createConfig.ts

import { deepFreeze } from "@clipboard-health/util-ts";
import dotenv from "dotenv";
import { fromZodError } from "zod-validation-error";

import { resolve } from "./internal/resolver";
import { type ConfigParams } from "./types";

dotenv.config();

/**
 * Type-safe static configuration management: a pure function to resolve, validate against a Zod
 * schema, and freeze configuration values.
 *
 * Configuration values resolve in order from highest precedence to lowest:
 * 1. Environment variables
 *    - Resolved converting configuration path from camelCase to UPPER_SNAKE. For example, the `{
 *      myApi: { port: 3000 } }` configuration resolves to `MY_API_PORT`.
 * 2. Environment-specific overrides, {@link ConfigValue.overrides}
 * 3. Default values, {@link ConfigValue.defaultValue}
 *
 * Supported configuration value types:
 * - bigint
 * - boolean
 * - date
 * - number
 * - string
 * - arrays and nested objects using the above types
 *
 * To override arrays with environment variables, use stringified JSON arrays, e.g. `["a","b"]`.
 *
 * **IMPORTANT**: To avoid runtime errors:
 * 1. Environment variables are strings, so use `z.coerce` Zod types for those you plan to override.
 *    Note that `z.coerce.boolean()` coerces any truthy value to `true`. To restrict to `"true" |
 *    "false"`, use the `booleanString` schema from `@clipboard-health/contract-core`.
 * 2. The resulting configuration is deeply frozen and will throw a runtime error if you attempt to
 *    modify it. The actual return type is `ReadonlyDeep<SchemaT>`, but the library returns a
 *    `Readonly<SchemaT>` because the former prevents clients from passing configuration values to
 *    functions that don't explicitly accept `readonly` types.
 *
 * @includeExample ./packages/config/examples/config.ts
 * @see [Usage example](../../examples/config.ts)
 *
 * @throws {Error} When configuration values fail schema validation
 * @returns A deeply frozen configuration object matching the provided schema
 */
export function createConfig<
  const SchemaT extends Record<string, unknown>,
  const EnvironmentT extends readonly string[],
>(params: Readonly<ConfigParams<SchemaT, EnvironmentT>>): Readonly<SchemaT> {
  const { config, environment, schema } = params;
  const { current } = environment;

  const result = schema.safeParse(resolve({ config, environment: current, path: [], schema }));
  if (!result.success) {
    throw new Error(`Configuration validation failed: ${fromZodError(result.error).toString()}`, {
      cause: result.error,
    });
  }

  return deepFreeze(result.data);
}

A usage example:

// ./examples/config.ts

import { ok } from "node:assert/strict";

import { createConfig } from "@clipboard-health/config";
import { z } from "zod";

const allowed = ["local", "development", "production"] as const;
type Allowed = (typeof allowed)[number];

function createEnvironmentConfig(current: Allowed) {
  return createConfig({
    config: {
      baseUrl: {
        defaultValue: "http://localhost:3000",
        description: "Base URL for API requests",
        overrides: {
          development: "https://dev.example.com",
          production: "https://api.example.com",
        },
      },
      database: {
        port: {
          defaultValue: 5432,
          description: "Database port",
        },
      },
    },
    environment: { allowed, current },
    schema: z.object({
      baseUrl: z.string().url(),
      database: z.object({
        // Use `z.coerce` to override with environment variables.
        port: z.coerce.number().min(1024).max(65_535),
      }),
    }),
  });
}

{
  // Uses default values.
  const config = createEnvironmentConfig("local");
  ok(config.baseUrl === "http://localhost:3000");
  ok(config.database.port === 5432);
}

{
  // Uses baseUrl environment override.
  const config = createEnvironmentConfig("development");
  ok(config.baseUrl === "https://dev.example.com");
  ok(config.database.port === 5432);
}

// Uses environment variable overrides.
const original = { ...process.env };
try {
  process.env["BASE_URL"] = "https://staging.example.com";
  process.env["DATABASE_PORT"] = "54320";

  const config = createEnvironmentConfig("local");
  ok(config.baseUrl === "https://staging.example.com");
  ok(config.database.port === 54_320);
} finally {
  process.env = { ...original };
}

Local development commands

See package.json scripts for a list of commands.

0.6.11

9 months ago

0.6.10

9 months ago

0.6.7

9 months ago

0.6.9

9 months ago

0.6.8

9 months ago

0.6.6

9 months ago

0.6.5

9 months ago

0.6.4

9 months ago

0.6.3

9 months ago

0.6.2

9 months ago

0.6.1

9 months ago

0.6.0

9 months ago

0.5.0

9 months ago

0.4.0

9 months ago

0.3.0

9 months ago

0.2.1

9 months ago

0.2.0

9 months ago

0.1.0

9 months ago