Skip to content

feat: add parameterDefaults for OAuth clients - #517

Merged
janpaepke merged 5 commits into
masterfrom
feat/oauth-defaults
Jun 30, 2026
Merged

feat: add parameterDefaults for OAuth clients#517
janpaepke merged 5 commits into
masterfrom
feat/oauth-defaults

Conversation

@janpaepke

Copy link
Copy Markdown
Collaborator

Summary

Implements the client-level parameterDefaults option from #426 (Option 2 — Defaults). When an OAuth / access-token client is created with parameterDefaults, the SDK fills testmode and/or profileId into every request whose endpoint accepts them — unless the per-call parameters already specify a value.

const mollie = createMollieClient({
  accessToken: 'access_...',
  parameterDefaults: { profileId: 'pfl_...', testmode: true },
});

await mollie.payments.page();                        // → ?profileId=pfl_...&testmode=true
await mollie.payments.get(id, { testmode: false });  // per-call wins → testmode=false

parameterDefaults lives only on the access-token branch of the options (it's an Xor with apiKey), since testmode/profileId are organization-credential parameters.

How it works

  • Plumbing (feat: add parameterDefaults client option): the value is read off the network client; withParameterDefaults(this, networkClient, config) wraps the named binder methods to fill the listed keys into the parameters object — never mutating the caller's object, and always letting a per-call value (including a literal false) win.
  • Wiring (feat: apply parameterDefaults to every binder method that accepts them): each binder calls withParameterDefaults in its constructor, before alias(), so aliased methods (list/all/cancel/delete) inherit the wrapped originals. The per-method key lists are validated at compile time against each method's parameter type — a key the endpoint doesn't accept won't compile.

Coverage

Applied to every binder method whose parameter type declares testmode/profileId: payments (+captures/refunds/chargebacks/routes), customers (+mandates/subscriptions/payments), chargebacks, refunds, subscriptions (+payments), methods, terminals (+pairing-codes), profiles, paymentLinks, balance-transfers, organizations, permissions, the settlement sub-resources, and applePay. Verified complete by a generated 98-assertion type probe — every valid key is covered and no eligible method was missed.

Deliberately excluded: the Orders API (mid-deprecation) and the profile sub-resources (profileMethods/giftcardIssuers/voucherIssuers — their profileId is a required path identifier a default can never fill).

Note (over-coverage / #489): defaults are injected wherever the SDK type declares the parameter, which includes a few spec-gap cases tracked in #489 (e.g. profileId on paymentLinks.page, testmode on settlement sub-resources). If the API rejects an over-declared query param, an OAuth client with that default set would 422 there — consistent with how those params already behave for explicit callers, and resolved once #489 lands.

Tests

tests/unit/defaults.test.ts covers query injection, request-body injection, no-arguments injection (the wrapper inserts a parameters object), per-call precedence (incl. literal false), alias forwarding, selective per-method keys, and the no-defaults control. Full unit suite green (46 suites / 152 tests).

Closes #426.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a client-level parameterDefaults option for OAuth/access-token clients so testmode and/or profileId can be automatically threaded into eligible requests while still allowing per-call parameters to override.

Changes:

  • Introduces ParameterDefaults + parameterDefaults option (OAuth-only via Xor) and threads it through the network client layer.
  • Adds withParameterDefaults plumbing to wrap binder methods and inject defaults without mutating caller-provided parameter objects.
  • Wires the wrapper into many binders (before alias() where applicable), and adds unit coverage + README docs.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/defaults.test.ts Adds unit tests covering default injection into query/body, no-args calls, override precedence (incl. false), and alias behavior.
src/plumbing/withParameterDefaults.ts New wrapper that injects configured defaults into binder method parameter objects without mutation.
src/Options.ts Defines ParameterDefaults and adds parameterDefaults to the access-token branch of client options.
src/communication/TransformingNetworkClient.ts Exposes parameterDefaults passthrough so binders can read it from the transforming client.
src/communication/NetworkClient.ts Stores parameterDefaults from options for later read-only access by binders/wrappers.
src/binders/terminals/TerminalsBinder.ts Applies testmode defaults to applicable terminals methods.
src/binders/terminals/pairing-codes/TerminalPairingCodesBinder.ts Applies profileId defaults to pairing-code methods that accept it.
src/binders/subscriptions/SubscriptionsBinder.ts Applies defaults to subscription paging/iteration before aliasing.
src/binders/subscriptions/payments/SubscriptionPaymentsBinder.ts Applies defaults to subscription-payment paging/iteration before aliasing.
src/binders/settlements/refunds/SettlementRefundsBinder.ts Applies defaults to settlement refunds paging/iteration.
src/binders/settlements/payments/SettlementPaymentsBinder.ts Applies defaults to settlement payments paging/iteration.
src/binders/settlements/chargebacks/SettlementChargebacksBinder.ts Applies defaults to settlement chargebacks paging/iteration.
src/binders/settlements/captures/SettlementCapturesBinder.ts Applies testmode defaults to settlement captures paging/iteration.
src/binders/refunds/RefundsBinder.ts Applies defaults to refunds paging/iteration before aliasing.
src/binders/profiles/ProfilesBinder.ts Applies testmode defaults to profiles.get.
src/binders/permissions/PermissionsBinder.ts Applies testmode defaults to permissions.get before aliasing list/page.
src/binders/payments/routes/PaymentRoutesBinder.ts Applies testmode defaults across routes CRUD + paging/iteration.
src/binders/payments/refunds/PaymentRefundsBinder.ts Applies testmode defaults across payment-refund operations + alias.
src/binders/payments/PaymentsBinder.ts Applies defaults across payment operations (create/page/iterate/etc.) before aliasing.
src/binders/payments/chargebacks/PaymentChargebacksBinder.ts Applies testmode defaults to payment-chargebacks methods + alias.
src/binders/payments/captures/PaymentCapturesBinder.ts Applies testmode defaults to payment-captures methods + alias.
src/binders/paymentLinks/PaymentLinksBinder.ts Applies defaults across payment-link operations.
src/binders/organizations/OrganizationsBinder.ts Applies testmode defaults to organizations.get.
src/binders/methods/MethodsBinder.ts Applies defaults to methods get/list before aliasing.
src/binders/customers/subscriptions/CustomerSubscriptionsBinder.ts Applies defaults to customer-subscription operations before aliasing.
src/binders/customers/payments/CustomerPaymentsBinder.ts Applies defaults to customer-payments operations before aliasing.
src/binders/customers/mandates/CustomerMandatesBinder.ts Applies testmode defaults to customer-mandates operations before aliasing.
src/binders/customers/CustomersBinder.ts Applies testmode defaults across customer operations before aliasing.
src/binders/chargebacks/ChargebacksBinder.ts Applies defaults to chargebacks paging/iteration before aliasing.
src/binders/balance-transfers/BalanceTransfersBinder.ts Applies testmode defaults and includes minor formatting for the page() method.
src/binders/applePay/ApplePayBinder.ts Applies profileId defaults to requestPaymentSession.
README.md Documents parameterDefaults usage for OAuth/access-token clients.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Options.ts
@janpaepke
janpaepke requested a review from edorivai June 30, 2026 10:56

@edorivai edorivai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed thoroughly (32 files): sound, well-typed, opt-in feature. LGTM.

Mechanism is correct:

  • Precedence: applyDefaults fills a key only when result[key] === undefined, so per-call values always win — including an explicit false. Covered by tests.
  • No shared-state mutation: copy-on-write (result === parameters ? {...parameters} : result); defaults are read-only. Each call gets its own object.
  • Opt-in/non-breaking: only on the accessToken branch of the Xor (unavailable with apiKey); when unset, input is returned by reference and behavior is unchanged.
  • Wrapping happens before alias(), so list/all/cancel inherit it.
  • 32 files = mechanical thread-through, and the exclusions are correct (TerminalPairingCodesBinder.create skipped — profileId is a required body param there; SettlementsBinder unwrapped — its params don't declare testmode/profileId).
  • Type-safe: DefaultsConfig<T> constrains keys to what each endpoint accepts (enforced by the green build); no user-facing any. CI green across the matrix.

One minor, non-blocking edge case for a follow-up: for methods without a leading id (page/iterate), passing an explicit undefined/null in the parameters slot causes the defaults to be silently dropped (no crash — self-corrects via renege). Unusual call pattern (callers normally pass {} or omit), so not a blocker. The acknowledged #489 over-coverage (a default on a spec-gap param could 422) is already documented in the PR body and consistent with existing explicit-caller behavior.

Unlike an API key, an OAuth/organization token isn't bound to a single profile or mode,
so callers must repeat `profileId` (and often `testmode`) on nearly every request.
`parameterDefaults` lets these be set once at client creation and filled into any request
that accepts them, unless the call provides its own value.

The value is held read-only on NetworkClient and forwarded read-only by
TransformingNetworkClient; a `withParameterDefaults` helper applies it, gated at compile
time to the keys each endpoint declares, so a default can't be sent where the API would
reject it. It lives in the accessToken branch of the options type because an API key fixes
profile and mode and the API rejects these parameters.

Wiring into the endpoints follows in the next commit.

Refs #426
Wires withParameterDefaults into each binder whose parameter types declare
testmode and/or profileId, so an OAuth client configured with
parameterDefaults fills those values into every such request unless the
per-call parameters already specify them.

The per-method key lists are validated at compile time against each
method's parameter type, so a key the endpoint does not accept cannot be
configured. withParameterDefaults runs before alias(), so aliased methods
(list/all/cancel/delete) inherit the wrapped originals.

The Orders API is intentionally excluded (mid-deprecation), and the profile
sub-resources are not wired because their profileId is a required path
identifier a default can never fill. Adds defaults.test.ts covering query
and body injection, per-call precedence (including a literal false), alias
forwarding, selective per-method keys, and the no-defaults path.
profileId is a required body parameter when creating a pairing code, so the caller must always pass it. Defaulting it on create via parameterDefaults implied it could be omitted; drop create from the wiring (page/iterate keep the default, where profileId is optional) and document why.
withParameterDefaults detected "no parameters" for any non-object in the
slot, then inserted the filled object after it. So calling a method with an
explicit undefined — a type-valid call, since the parameter is optional —
e.g. payments.page(undefined) or payments.get(id, undefined), left the
undefined in the parameters position and silently dropped the defaults.

Now the slot is replaced when it is occupied (an object, or an explicit
undefined/null standing in for {}); insertion only happens when no
parameters argument was passed (a leading id, or nothing). This also
flattens the branch. Adds tests covering the undefined case on both
id-less and id-methods.
@janpaepke
janpaepke force-pushed the feat/oauth-defaults branch from c83335b to b3adb8e Compare June 30, 2026 14:13
@janpaepke

Copy link
Copy Markdown
Collaborator Author

@edorivai thanks for the thorough review — addressed the undefined/null edge case you flagged.

payments.page(undefined) was silently dropping the defaults: the wrapper detected "no parameters" for the non-object slot and inserted the filled object after the explicit undefined, so the method read its params from the undefined and ignored the injected object. One nuance — it was a bit broader than page/iterate: any method called with an explicit undefined in the parameters slot was affected, including id-methods like payments.get(id, undefined).

Fixed in c83335b: an explicit undefined/null in the parameters slot is now treated as {} — the slot is replaced rather than inserted-after; insertion only happens when no parameters argument was passed at all (a leading id, or nothing). null isn't type-valid there anyway, but it's handled for free. The change also flattens the branch (the hasParameters special-case folds into a single typeof parameters != 'string' check), and the "untouched when nothing fills" property you called out is preserved.

Added tests/unit/defaults.test.ts cases covering undefined on both an id-less method (page) and an id-method (get), each asserting the same result as passing {}.

@janpaepke
janpaepke merged commit 7c7a7be into master Jun 30, 2026
12 checks passed
@janpaepke
janpaepke deleted the feat/oauth-defaults branch June 30, 2026 14:15
@janpaepke janpaepke mentioned this pull request Jun 30, 2026
@janpaepke janpaepke added this to the 4.6.0 milestone Jun 30, 2026
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.

Enhance Mollie Connect API Client Configuration for OAuth Flow

3 participants