Skip to content

feat(coerce): add domain coercers and route 12factor-env's validators through them - #1721

Merged
pyramation merged 1 commit into
mainfrom
feat/coerce-domain-validators
Aug 14, 2026
Merged

feat(coerce): add domain coercers and route 12factor-env's validators through them#1721
pyramation merged 1 commit into
mainfrom
feat/coerce-domain-validators

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

@constructive-io/coerce only had the primitive shapes, so the named shapes still lived as envalid validators — meaning port, url, host, email could vet process.env but not a request body, and uuid existed nowhere (a regex in @pgpmjs/server-utils). This adds them as runtime coercers and makes 12factor-env's validators thin envalid wrappers over them, so a variable and a request body agree on what each shape accepts:

// packages/12factor-env/src/validators.ts — envalid supplies the spec plumbing, coerce the acceptance
const validator = <T>(coerce: (input: unknown) => T | null, expected: string) =>
  (spec: Spec<T> = {}) => makeValidator<T>((input: string) => {
    const value = coerce(input);
    if (value === null) throw new EnvError(`Invalid ${expected} input: "${input}"`);
    return value;
  })(spec);

export const port = validator(asPort, 'port');   // + url, host, email, uuid

list/num/int/duration/enumerated keep their own bounds, choices and error text — the parsing underneath is now asStringList/asNumeric/asNumberIn/asDuration/asOneOf, and the duplicated DURATION_UNITS_MS table and the Number(input)/String(input).split() bodies are gone. port/url/host/email are no longer re-exported from envalid; they resolve to the house versions, and uuid is new. Env semantics are unchanged (unset resolves the default, set-but-invalid throws into the consolidated report), with 5 new tests pinning the shapes and their rejections.

New coercers, each with a require* twin:

asNumeric, asNumericInteger a number carried as text — the env/query form of asNumber
asNumberIn, asIntegerIn inclusive { min, max }; out-of-range is a miss, not a clamp
asPort integer 1..65535
asBigInt bigint / integer / digit string — a 64-bit id crossing JSON
asUrl scheme required, so 'example.com' (a path) is not a URL
asHostname no scheme, port or path — 'localhost:5432' in a PGHOST is how a port reaches a DNS lookup
asEmail, asUuid shape only
asDuration '500ms'/'30s'/'5m'/'2h'/'1d'/'1w' → ms; unit-less is ms
asJson object/array, or a string parsing to one; a bare scalar is not a document

The line the package draws: asNumber/asInteger stay strict ('5' is not a number) so a JSON body cannot smuggle a string past a declared number; the coercers that do read text say so in their names, because an env var carries nothing else.

READMEs

Package READMEs were hand-rolling their own footer — ## License / MIT, or codegen's ---\nBuilt by the Constructive team.\n## Disclaimer … — while makage already appends the root FOOTER.md at build time (copy.js: ${readme}\n\n---\n\n${footer}). 101 READMEs lose that duplicated tail; the generator that emitted it is fixed at the source (getReadmeFooter() deleted from graphql/codegen, snapshots updated) rather than patched in its output. agentic/*'s ## Credits stay — those attribute the upstream pi agent, not us. The root README.md is untouched: nothing appends the footer to it.

Link to Devin session: https://app.devin.ai/sessions/4283bbf118594207ad8b1ef1ce856ce8
Requested by: @pyramation

… through them

Strip hand-written footers from package READMEs; makage appends FOOTER.md.
@pyramation pyramation self-assigned this Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit c0b8a06 into main Aug 14, 2026
21 checks passed
@pyramation
pyramation deleted the feat/coerce-domain-validators branch August 14, 2026 00:02
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.

1 participant