Skip to content

feat(types)!: query param encoding + optional search totals (5.0.0) - #116

Merged
javorosas merged 10 commits into
mainfrom
fix/nested-query-params
Sep 10, 2026
Merged

feat(types)!: query param encoding + optional search totals (5.0.0)#116
javorosas merged 10 commits into
mainfrom
fix/nested-query-params

Conversation

@javorosas

@javorosas javorosas commented Sep 9, 2026

Copy link
Copy Markdown
Member

What

Query params are serialized with the encoding the API documents, and the search envelope now reflects what the API actually returns.

Query serialization

  • Nested objects use bracket notation: date[gte]=2026-01-01&date[lt]=2026-02-01 (the Fetch migration replaced the previous axios serializer, which sent date=[object Object]).
  • Arrays repeat the key: status=valid&status=canceled, matching the OpenAPI default (form with explode) and the other official SDKs.
  • null, undefined, and empty collections are skipped; an explicit empty string still sends value=.

Types — breaking, 5.0.0

  • SearchResult<T> types the whole envelope: page, total_pages, and total_results are optional, because cursor pages only report the totals on the first request of the sequence and a search with no matches reports page: 0.
  • CursorSearchResult<T> was removed: cursor responses are SearchResult<T> with optional previous_cursor / next_cursor, and the list() overloads that selected the result type were removed with it.
  • CursorSearchParams and PageSearchParams stay for typing search params.

Tests

pnpm test — build, 31 node tests, 10 web tests, and the tsd type assertions all pass.

new URLSearchParams() coerces object values to "[object Object]", so list and
search calls that pass a date range object ({ gte, lt }) sent
date=[object Object] and every date-filtered request failed with a 400.

Flatten params into [key, value] pairs before encoding: plain objects expand to
bracket keys (date[gte]=...), arrays to repeated empty-bracket keys
(status[]=a&status[]=b), matching the v2 API contract and the curl examples in
the docs. null/undefined values and empty collections are skipped; Date values
are sent as ISO 8601 strings.

Bump to 4.22.0 and add regression tests.
SearchResult now mirrors what the v2 API returns: page/total_pages/
total_results are optional (later cursor pages omit totals) and new optional
totals_are_capped, next_cursor, and previous_cursor support cursor pagination
and capped totals.
@javorosas
javorosas requested review from raul-facturapi and a balanced review from Copilot September 9, 2026 13:41
@javorosas javorosas self-assigned this Sep 9, 2026

This comment was marked as resolved.

The new SearchResult pagination fields are an additive feature (minor), so
they belong under Added; the nested query serialization fix stays under Fixed.
…iter

- Restrict nested bracket expansion to plain records (Object.prototype or
  null prototype); Date keeps ISO conversion; other object values (URL,
  RegExp, custom instances) keep their previous String(value) encoding
  instead of being silently dropped or recursed.
- buildQueryString returns an empty string when every value is omitted, and
  the request builder only appends '?' when there is something to serialize,
  so all-omitted params produce the bare URL.
- Regression tests for both cases.

This comment was marked as resolved.

…s additive

Making SearchResult.page/total_pages/total_results optional would be a
source-breaking change for strict TypeScript consumers under a minor release.
Restore the required signatures (previous contract unchanged) and keep the
cursor/capped metadata as new optional fields; document that the API only
reports totals on page-mode responses and the first request of a cursor
search, so cursor consumers should rely on next_cursor.
@javorosas

Copy link
Copy Markdown
Member Author

Semver resuelto de forma aditiva (commit del tipo): SearchResult.page/total_pages/total_results conservan sus firmas requeridas (contrato previo intacto → minor 4.22.0 válido); los campos de cursor/capping se mantienen opcionales y nuevos. Se documenta que la API solo reporta totales en page mode y en la primera request de cursor. Tests/build/tsd verdes. Solicitando nueva revisión.

This comment was marked as resolved.

The API reports totals only in page mode and on the first request of a cursor
search; later cursor pages omit page/total_pages/total_results. Model that
truthfully as optional fields (source-breaking for strict consumers that
dereferenced them) and release as a major, with the new optional
totals_are_capped/next_cursor/previous_cursor under Added.
@javorosas javorosas changed the title fix: serialize nested and array query params with bracket notation feat: bracket query serialization + optional search totals (5.0.0) Sep 10, 2026
…major)

Instead of making SearchResult.page/total_pages/total_results optional (source
breaking), add CursorSearchResult<T>/CursorSearchParams and cursor-aware
list() overloads: page mode keeps the existing signatures, and cursor searches
get truthful optional totals plus next/previous cursors. Release stays 4.22.0
(minor, additive).
@javorosas javorosas changed the title feat: bracket query serialization + optional search totals (5.0.0) fix: bracket query serialization + cursor-aware list typing (4.22.0) Sep 10, 2026
@javorosas

Copy link
Copy Markdown
Member Author

Rediseño aditivo (d89dc37): en lugar de opcionalizar page/total_pages/total_results (rompía strict consumers), se agregan CursorSearchResult/CursorSearchParams + overloads de list(); page mode conserva su contrato y los cursores quedan tipados con totales opcionales. Release 4.22.0 (minor).

@javorosas

Copy link
Copy Markdown
Member Author

Resuelto con el rediseño (d89dc37): page mode conserva SearchResult con firmas requeridas (correcto para ese modo) y los cursores usan el tipo nuevo CursorSearchResult (totales opcionales + next/previous_cursor requeridos) vía overloads de list(), así que ya no hay contradicción entre tipos y comportamiento documentado.

Symmetric param types for list(): PageSearchParams (page mode, the default)
and CursorSearchParams (cursor mode), each an object with known fields
intersected with Record<string, any>. A page-mode overload returns
SearchResult<T> (unchanged) and cursor params keep returning
CursorSearchResult<T>. Purely additive: existing call sites, including loose
Record<string, any> params, keep compiling. Type tests cover both modes, the
loose-params fallback, and that page totals stay required.

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.

🟢 Approval recommended

The implementation matches the stated API contracts and includes appropriate runtime and type coverage.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@javorosas

Copy link
Copy Markdown
Member Author

One more commit: arrays now serialize as repeated keys (status=valid&status=canceled) instead of status[]=valid&status[]=canceled. That is the OpenAPI default form for arrays (form with explode) and what the other official SDKs send. Nested objects keep bracket notation (date[gte]=...), and null/undefined/empty collections are still skipped. Wire-only change: pnpm run test:node passes (31 tests).

@javorosas
javorosas requested a balanced review from Copilot September 10, 2026 16:15

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@javorosas javorosas changed the title fix: bracket query serialization + cursor-aware list typing (4.22.0) feat(types)!: query param encoding + optional search totals (5.0.0) Sep 10, 2026
@javorosas
javorosas merged commit 7c503c1 into main Sep 10, 2026
6 checks passed
@javorosas
javorosas deleted the fix/nested-query-params branch September 10, 2026 16:52
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.

3 participants