Skip to content

feat(api): GET /v1/customer/search — company-scoped substring lookup#64

Merged
CryptoJones merged 1 commit into
masterfrom
feat/customer-search
May 18, 2026
Merged

feat(api): GET /v1/customer/search — company-scoped substring lookup#64
CryptoJones merged 1 commit into
masterfrom
feat/customer-search

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Summary

New GET /v1/customer/search endpoint. Substring match (ILIKE) across custCompanyName / custFName / custLName, company-scoped via authKey or explicit companyId. Closes the gap between the per-id GET and the paginated bycompany list.

GET /v1/customer/search?q=acme&limit=50

Stricter auth than the list endpoint — master keys MUST specify companyId (no global cross-tenant search). q enforces a 2-char minimum so an autocomplete that fires on each keystroke doesn't full-scan.

Test plan

  • vitest: 234 passing + 4 integration skipped (was 227 + 4 skipped)
  • Auth contract (403 without header)
  • q required + 2-char minimum + strict() rejects unknown params
  • limit cap enforced (>500 rejected)
  • Route mounted before /:id so "search" isn't parsed as a customer id
  • Live integration with real PG ILIKE (deferred — same mock limitation as other API tests)

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Search by substring across custCompanyName / custFName / custLName
(case-insensitive ILIKE). Common SDK pattern is "user types 'acm'
in the customer autocomplete," and the existing
GET /v1/customer/bycompany/:id paginated list doesn't help unless
the caller already knows the company id AND fetches the whole
table to filter client-side. Search closes that gap.

Auth shape (deliberately stricter than the existing list endpoint):

  - missing authKey                                  -> 403
  - non-master + companyId mismatching auth scope   -> 403
  - non-master without companyId                    -> auto-scope to own
  - master without companyId                        -> 400

The "master must specify companyId" requirement is intentional —
a global cross-tenant substring search is a footgun (latency on
huge tables; accidental data exposure if the master key wasn't
authorized to read every tenant's data). Forcing the explicit
scope keeps the surface predictable.

`q` enforces a 2-char minimum at the zod boundary so a dropdown
that fires on every keystroke doesn't full-scan the table on the
first letter.

Route declared before /:id so express doesn't treat "search" as
a customer id and route to getCustomerById.

OpenAPI gets the full path entry with parameter docs + the
response envelope schema. Tests cover the auth contract (403),
zod validation paths (q required, min length, strict() rejects
unknown params, limit cap), and route mounting (verifies the
search-before-:id ordering).

Tests: 32 files / 234 passing + 4 integration skipped (was 31 / 227).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CryptoJones CryptoJones merged commit a17cab6 into master May 18, 2026
1 check was pending
@CryptoJones CryptoJones deleted the feat/customer-search branch May 18, 2026 02:02
CryptoJones added a commit that referenced this pull request May 18, 2026
#68 added /v1/timeentry/export.csv but placed the route AFTER the
existing /v1/timeentry/:id block. Express tries patterns top-down,
so a GET to /v1/timeentry/export.csv matched the :id route first,
the intIdParam validator parsed "export.csv" → NaN → 400 with
"expected number". The test that asserts 403 on missing authKey
was flaking on this path. The export handler was never reached.

Mirrors the search-before-:id ordering #64 used for customer.
Added a comment block flagging the rule for future contributors.

All four timeentry CRUD routes still resolve correctly — they
sit AFTER the literals now, which is the correct order.

Suite: 261 / 261 + 4 integration skipped (post-fix).

Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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