Protected ReadonlycacheProtected ReadonlydbStaticinstanceThe process default: built from each dependency's own instance().
Optionalid?: stringOptionalincludeDeleted?: booleanResolve a soft-deleted customer instead of returning null. Folds onto
search's parameter of the same name. Operator surfaces that list
deleted customers pass it so the row they render can still be opened;
nothing that transacts should.
OptionalworkspaceId?: stringDrop this customer's cached reads.
Lives here rather than on the writer because it's the cache's own
concern — it names the folded-get keys and the membership tag — so both
the reader (CustomerQueryService) and the writer (CustomerService)
inherit it. The actual caller is customerOnboardingActivities
(@cfxlabsinc/onboarding-services), which calls this once a primary
organization exists for the customer: a customer is invisible to
get/search until its primary org exists, so every read taken earlier
in onboarding cached a negative entry with a 24h L2 TTL, and this is what
clears it.
Clear each customer's membership tag and each record tag wherever cached,
and always this namespace's collection tag. keys are exact deletes in this
instance's key space: the only bust a null client honours.
OptionalcreatedAt?: DbTimestampCriteriaFilter to customers created within this window.
OptionalidAfter?: stringKeyset cursor: customers whose id sorts after this one. Pair with orderBy on externalId.
Optionalids?: string[]OptionalincludeDeleted?: booleanReturn soft-deleted customers too. Ignored when statuses names
DELETED: that list already states which deletedness the caller wants,
and applying the default on top of ["DELETED"] would return nothing
for a query that plainly asked for deleted customers.
OptionalincludeWithoutMainOrg?: IncludeWithoutMainOrgTurn the primary-organization INNER JOIN into a LEFT JOIN, so customers
that have no primary organization yet are returned too — with
organizationId / legalEntityName absent (see
MaybeOnboardedCustomer).
A deliberate, narrow escape hatch from the rule that a Customer
is a fully-onboarded one. Two callers:
OrganizationService.create({ isMainOrg: true }), which has to read the
customer whose primary org it is in the middle of inserting; without this
it can never see it and retries forever. And customer-dashboard's
/apply/[id]/verify, which reads only status to show provisioning,
for a workspace the caller's membership already proved. Nothing else
user-facing should pass it.
Results are NOT cached when this is set: a half-built customer changes shape within seconds, and the 24h L2 TTL would outlive that by a long way.
OptionalisInternal?: booleanCFX's own customers (true) or everyone else (false).
OptionalmatchAdminText?: booleanAlso match q against admin-only text: data (operator notes, fee
settings), description and attio_id. Admin surfaces only; a customer
or bank caller must leave it unset, or q probes other customers' notes.
Optionalname?: stringOptionalnameLike?: stringOptionaloperatingAccountLinked?: booleanWhether operatingAccountId is set.
OptionalorderBy?: DbOrderByCriterion<Optionalpage?: numberDefaults to 1
OptionalpageSize?: numberDefaults to 50
OptionalprodCustomerIds?: string[]Filter to dev customers twinning these prod customers (cust_… in the prod database).
OptionalprogramIds?: string[]Filter to customers holding a grant on any of these programs (prog_…).
Optionalq?: stringUnified free-text search. Case-insensitive substring match against the
customer name, the customer external id (cust_…), or the legal entity
(organization) name. Whitespace-only values are a no-op.
Optionalstatuses?: ("PENDING" | "ACTIVE" | "FAILED" | "DISABLED" | "DELETED")[]Filter to customers in these statuses — the provisioning lifecycle
(PENDING / ACTIVE / FAILED) and the two operator states
(DISABLED / DELETED).
OptionalupdatedAt?: DbTimestampCriteriaFilter to customers updated within this window.
OptionalworkspaceIds?: string[]ProtectedsearchTags for a page: the scope its query searched (each customer's membership
tag, else the collection tag), so an empty page is bustable too, plus the
records it projects and the fields that chose or ordered its rows.
StaticgetAs getCacheKey, for the get({ workspaceId }) fold.
StaticgetThe exact cache key CustomerQueryService.get({ id }) reads through.
get folds onto search, so its entry is an ordinary single-row search
page — these args must stay identical to the ones get passes
({ ids: [id], pageSize: 1 }, plus search's page default of 1). Every
other filter is undefined there, and serviceCacheKey canonicalizes with
JCS, which drops undefined properties, so naming only the three that are
set produces the same digest. customerCache.test.ts pins the equivalence.
Returns the key arguments; the cache hashes and surface-prefixes them. Never pre-hash at a call site.
Read-only access to customers.
Customer service is special: While it can interact with multiple customers like an admin service, when used in a non-admin context, it should only be used to interact with the current customer.
get()andsearch()INNER JOIN the customer's primary organization (organization.customer_id = customer.id AND organization.is_main_org), so every Customer they return carries a non-nullorganizationIdandlegalEntityName.That join also DEFINES what a
Customeris here: a fully-onboarded one. Customer and primary org are no longer created together —CustomerService.create()writes the customer row and nothing else, and@cfxlabsinc/onboarding-servicesthen callsOrganizationService.create({ isMainOrg: true }). Between those two steps, and after a failed onboarding run, a customer row exists with no primary org and is deliberately invisible toget/search. The onboarding workflow — the only thing that must see a customer in that half-built state — reads it throughsearch({ includeWithoutMainOrg: true }), which swaps that join for a LEFT one.Extends CustomerCache, which owns the
customer-searchnamespace, the seam that busts it, and the key builders its readers and writers share.This class deliberately depends on nothing but
db.