feat(coerce): add @constructive-io/coerce and re-base 12factor-env's lenient parsers on it - #1720
Merged
Conversation
…lenient parsers on it
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every service re-types the same four coercions inline, each subtly different —
asString/asInteger/requireString/isoappeared again in constructive-db#3081's gateway lane, and the same shapes already live in its webhook gateway, node runtime (site/auth/email), knative reconciler specs, and ~14 spots here. This adds the published home for them and makes12factor-enva consumer rather than a second implementation. Design + rationale: constructive-io/constructive-planning#1644 (comment).packages/coerce→@constructive-io/coerce, zero dependencies. Two families overunknown:as*is lenient (T | null),require*throws a labelledCoerceError.The rules are the interesting part, and each is a bug we've shipped:
'5'is not an integer,1is not a string. OnlyasBoolean/asDate/asStringListread strings, because env vars and query strings carry nothing else.''/' '→null, so an empty value can never read as a supplied identifier (the security property PR #3081 depends on).asStringArray(['a', ''])isnull, not['a']: dropping bad entries is how an allowlist quietly shrinks.CoerceErrorcarries the failinglabel, never an HTTP status, so the package stays usable off the request path. A server maps it at its own boundary:if (err instanceof CoerceError) throw new HttpError(400, err.message).asIsoStringpasses a string through verbatim rather than round-tripping it throughDate, so a caller's cursor survives byte-for-byte.12factor-envlayers on it instead of being merged into it: the package keeps envalid schemas, redaction and cross-field checks, and its three lenient parsers become env-shaped adapters where only the env convention ("unset or blank isundefined, not a default") remains local.boolish()collapses toasBoolean(value) ?? false. Signatures and the existing 70 tests are unchanged; the one intentional widening is thatparseEnvBoolean/boolishnow also accepton/t/y(and readoff/f/nas false), where before anything outsidetrue|1|yeswas false.envalid-backedlist()/num()invalidators.tsare untouched — theirEnvErrormessages are schema-level.Folding coerce into
12factor-envwas rejected: the name promises env vars, so payload coercion hidden there invites the next inline reinvention, and every consumer (e.g. the node gateway) would inherit envalid for four ten-line functions.Follow-up, after this publishes: constructive-db depends on the package and deletes its inline copies. That swap is not a blocker for #3081.
Link to Devin session: https://app.devin.ai/sessions/4283bbf118594207ad8b1ef1ce856ce8
Requested by: @pyramation