Skip to content

docs: inventory of all 533 HTTP endpoints - #4551

Draft
TaprootFreak wants to merge 19 commits into
developfrom
docs/endpoint-inventory
Draft

docs: inventory of all 533 HTTP endpoints#4551
TaprootFreak wants to merge 19 commits into
developfrom
docs/endpoint-inventory

Conversation

@TaprootFreak

Copy link
Copy Markdown
Collaborator

What this adds

docs/endpoints.md — a complete inventory of every HTTP endpoint this service exposes:
533 handlers across 93 controller files, grouped by file, listing method, path, handler
and whether the endpoint is hidden from the Swagger schema (223 are).

Documentation only. No source file is touched.

How the list was built and verified

Derived from the @Get / @Post / @Put / @Patch / @Delete decorators in
src/**/*.controller.ts. Each endpoint is attributed to the @Controller scope that precedes
it, which matters in five places that a naive scan gets wrong:

  • four files declare two controller classes with different base paths
    (e.g. custody and custody/admin)
  • one declares @Controller() without an argument, placing its routes at the root
    (/pl, /plp)

The result was cross-checked in both directions against the route list the framework registers
at startup: all 526 distinct method/path pairs match, with no entry on either side left over.

One discrepancy, documented in the file

POST /paymentLink/integrations/kucoin/webhook/cancel exists in the source but is never
registered at runtime: its handler in c2b-payment-link.controller.ts carries two @Post
decorators, and the framework stores a single path per handler, so only .../webhook/success
takes effect.

This PR does not fix that — it only records it, so the inventory does not silently claim an
endpoint that cannot be reached.

Why

This is the base for a follow-up that adds a column marking read paths — endpoints that
only read data and render it, such as invoices, receipts, history and exports — as opposed to
write paths that must load complete entities in order to persist them.

That distinction cannot be derived from the HTTP verb (PUT /transaction/:id/invoice writes
nothing, it renders a PDF), and it decides where a query may select individual fields instead
of loading a whole object graph.

Adds docs/endpoints.md, a complete list of every HTTP endpoint the service exposes, grouped by controller file and listing method, path, handler and whether the endpoint is hidden from the Swagger schema.

The list is derived from the routing decorators in src/**/*.controller.ts. Endpoints are attributed to the @controller scope that precedes them, which matters for the four files declaring two controller classes with different base paths and for the one declaring @controller() without an argument. The result was cross-checked in both directions against the route list the framework registers at startup: all 526 distinct method/path pairs match.

The cross-check surfaced one discrepancy, documented in the file: the handler for the KuCoin Pay webhook carries two @post decorators, so only .../webhook/success is registered and .../webhook/cancel never takes effect. That defect is left untouched here.

This is preparation for a follow-up that adds a column marking read paths - endpoints that only read and render data - so queries there can select individual fields instead of loading whole object graphs.
Every PR that adds, removes, renames or re-scopes a route must now update docs/endpoints.md in the same PR. Recorded in two places: as item 5 of the PR completeness checklist, and as a section next to the REST endpoint conventions explaining how to edit the list and how to verify it.

The section names the two traps that make a hand-edited list wrong: a file may declare several @controller classes, so a route belongs to the scope preceding it rather than to the first in the file, and @controller() without an argument places its routes at the root. It also points at the startup route log as the way to confirm that a new route is actually registered.
Replaces the per-controller tables with a single table sorted by path, and adds three measured columns.

Eager: whether the endpoint's load path triggers TypeORM's automatic eager relations. The rule is mechanical - eager applies to the find* family, not to createQueryBuilder with an explicit field list or to raw SQL. Determined from the call chain between handler and repository; 82% of endpoints resolve, the rest are marked '?' rather than guessed.

Cols and Fields: columns the query actually selects, measured against the real entity metadata by building the query and counting its SELECT list, against the field count of the declared response DTO. Ratio is the quotient where both are known. These are measurements, not estimates - a plain findOne() on UserData selects 253 columns across 8 joins, one on LimitRequest 434 across 15.

Gaps are marked with an em dash and explained in a coverage section: they are limits of static analysis, not zeros.

CONTRIBUTING is updated to match the new structure and states the rule for the Eager column, so it can be filled in by hand when an endpoint is added.
How the table is sorted is visible from the table itself. Describing it in the rule adds noise around the part that is not obvious - which columns to fill in by hand and how to decide the Eager value.
Two parser defects inflated the unresolved count, both fixed:

- 60 class names occur in more than one file (among them two KycController classes). Storing methods by class name alone overwrote one with the other, so 39 handlers could not be found at all. Entries are now merged instead of overwritten.
- A cycle in the call graph was counted as an unresolved call. A cycle contributes nothing and is not a doubtful case.

Eager resolution rises from 82% to 88%.

The Fields column now distinguishes 'does not apply' from 'unknown'. Of 313 endpoints without a field count, 113 return void and 172 return no DTO at all - neither has a field set to count. Only 28 are genuine recognition gaps. The first two now show n/a, the last dash, and the coverage table breaks the numbers out. Reading 41% as poor coverage was misleading.
docs/read-path-projections.md explains what the Eager column is for, so the inventory is readable without prior context.

Contents: where the overfetching comes from (95 eager relations expanding recursively, 368 load sites requesting object graphs against 24 using a projection); the vocabulary; the five criteria an endpoint must meet before its read path is converted; the risk being guarded against, with the concrete getter that returns false instead of true when one field is missing; and the four-level test definition.

The test levels are completeness (no DTO field empty on a fully populated fixture), variants (one fixture per branch that changes the required field set), mutation (removing any field must fail level 1, which proves it looks at anything), and consistency against a second source where a value was materialised while the original is still present.

Two findings are recorded because they are easy to get wrong: a select inside find options does not avoid eager relations (measured: 98 columns plain, 81 with a three-field select, 3 with a query builder), and a mocked repository cannot test any of this because it returns what the mock defines regardless of which columns were requested.

A column budget was considered and rejected; the reasoning is documented so it is not proposed again.
Methods whose parameters are destructured - '@query() { a, b }: SomeDto' - had their body extracted from the first brace after the signature, which is the destructuring pattern, not the method body. AssetController.getAllAsset for instance came out as ' blockchains, includePrivate', so no database access could be found in it.

The parameter list is now skipped before looking for the body brace.

Effect: 35 more endpoints are correctly identified as loading object graphs (295 instead of 260), unresolved drops from 65 to 60, and the number with both a column count and a field count rises from 113 to 134.
Two thirds of the unresolved entries came from one pattern: save, update, create, delete and count are inherited from the repository base class, so they were not found on the concrete repository and the chain was treated as unresolved. Those are write and count operations - they do not trigger eager loading at all, so marking them unknown was simply wrong.

The same applies to calls into built-in classes such as Map and DataSource.

Unresolved drops from 60 to 12, resolution reaches 98%. The eager count is unchanged at 295 - the additional entries all resolve to 'no', as expected for write operations.
Three parser defects, each found by an entry that looked implausible:

- Line comments were parsed as code. A comment reading 'falls back to this.user.address (= transaction.user)' was read as a method call on an untyped field.
- Class bodies were located by brace matching. In a 3292-line service a brace inside a string shifted the count and two thirds of the class was lost, so 7 endpoints had no reachable handler. Methods are now located file-wide and attributed to the preceding class.
- Block comment removal was tried and reverted: a regex literal in the source can contain the opening sequence, after which the expression eats hundreds of lines.

Unresolved drops from 12 to 2 (99.6% resolved). The two that remain are genuine: a dynamically selected strategy and a storage call.
The last two came from branch ordering in the resolver: the check for known non-database types sat after 'is the class parsed at all', so the exception stopped applying as soon as the class itself had been parsed - StorageService and the strategy registry are parsed, so calls into them counted as unresolved.

Every one of the 533 endpoints now resolves to yes or no. 295 load object graphs, 238 do not.
The Eager column conflated two very different things under 'no': endpoints that select an explicit field list, and endpoints that touch no database at all. Only the former can have an incomplete field list, and only they are subject to the projection tests.

The column is now called Load and carries three values: eager (295), projected (27), none (211). A new section states which group the test definition applies to, and why it does not apply to the other two - none has no field list to get wrong, eager loads everything anyway and becomes subject to the tests at the moment it is converted.

Also recorded in read-path-projections.md: the repository already has the mechanism for database-backed tests. Fourteen migration specs gate on MIGRATION_TEST_PG, and the pull-request workflow already runs a Postgres service. The projection tests should use that rather than introduce a second mechanism - only the schema (from entity metadata) and the generated fixtures are missing.
Verified against the workflow rather than described from memory: a throwaway Postgres 16 runs as a service in all three test shards, because Jest distributes the suites across shards and each needs its own instance.

Also recorded is how the migration specs isolate themselves - each creates its own Postgres schema so parallel specs cannot collide. The projection tests need the same isolation, but take their schema from the entity metadata via synchronize rather than from replayed migrations, since the reference for a projection is the entity definition.

That leaves exactly two things to build: the schema setup and the generated fixtures. The document previously implied the whole test base had to be created.
The Load column on the endpoint table was wrong and could not be made right. It took the maximum over the whole call graph, so an endpoint counted as eager as soon as any call anywhere in its chain used find - a permission check, a lookup, a notification. Checked against independently verified cases it scored 4 of 7: the financial log and the two generic query endpoints came out eager although their data path is hand-written SQL.

An endpoint does not have one load path. It reaches several, and a single value per endpoint cannot express that faithfully, regardless of how good the parser gets.

Replaced by docs/load-sites.md: every one of the 1114 places in the code that reads from the database, with mechanism, target entity, and the column count measured against the real entity metadata. That statement is local and needs no call-chain heuristic.

The numbers: 971 sites use the find family and therefore apply eager relations, 131 use a query builder, 12 raw SQL. Of the query builders exactly ONE carries an explicit field list; 105 call .select('alias'), which reads like a projection but selects the entity alias and still loads every column. Median 118 columns per site, 14 sites above 1000.

Measurement coverage is stated honestly: 349 exact where the relations tree is written at the call site, 435 lower bounds where it arrives as a parameter, 330 not measurable.

endpoints.md keeps only what is verified: the routes themselves, cross-checked in both directions against the framework's startup registration.
The endpoint list identified a row by method and path, which is not unique: six
paths exist twice, once on a deprecated handler and once on its replacement, and
the two rows were indistinguishable. Adds a version column, sourced from @Version
on the handler, the @controller scope, or the configured default. Verified against
production access logs, where /v1/kyc/admin and /v2/kyc/client each appear only
under the version recorded here.

Corrects seven handler names. The extraction took the next line that begins,
indented, with an identifier and a parenthesis; where the decorator block spans
several lines that is the guard, not the method, so seven routes were attributed
to AuthGuard. Decorators are now skipped by counting parentheses, with string
literals and comments respected.

Removes two query builders from the load-site inventory. Both carry .update() and
are write statements, which load nothing, leaving 1112 sites and 129 loading query
builders.

Adds a data access column summarising, per endpoint, whether it reaches any load
site that fetches whole rows. Of 533 endpoints, 432 do, 97 read nothing at all,
2 read only the fields they return, and 2 project only when the caller supplies a
field list. The column is the union over every reachable site and answers one
question only, whether an endpoint loads more than it needs; load-sites.md remains
the place that says where the work actually happens.

Replaces two figures in read-path-projections.md that no longer held: a claim of
368 load sites against 24 projections, which the load-site inventory in the same
PR contradicts, and a per-field ratio that treated command endpoints as read paths.
…tements

Completes the inventory of the current state.

Adds a deprecation column. 24 handlers carry @apioperation({ deprecated: true }),
21 of which fetch whole rows. This is what the duplicated paths are about, and it
does not follow the version: GET /kyc/countries is marked on both the v1 and the
v2 handler.

Lists the 27 endpoints whose classification rests on reading the source rather
than on the call graph, each with its reason. Their call chains end at a target
chosen at runtime - a strategy registry, an exchange client, an in-memory cache -
so the judgement was made by hand. Recording it makes it checkable instead of
invisible.

States the limits of the classification in numbers rather than in prose: 435 of
533 endpoints rest on a call graph that is not fully resolved, which is why 432 is
a lower bound and not an exact figure; all 97 endpoints marked as reading nothing
are fully resolved; 3 endpoints have no measured column width. Notes that
KycController, KycClientController and KycService each exist twice, so a row is
identified by its file and not by the handler name alone.

Excludes statements that load nothing from the load-site inventory: 2 query
builders carrying .update(), 6 advisory locks and 1 raw INSERT. 1105 sites remain.

Corrects two entries in the load-site table. SQL keywords inside template literals
were read as method signatures, which put THEN in the method column; template
contents are now blanked before the signature scan. Sites with no enclosing method
printed None and now print a dash.

Corrects the count of reads that name their columns. It is six, not one: the query
builder with an explicit field list plus the five raw statements, each of which
lists its columns. The test definition applies to five of them - the sixth takes
its field list from the request.
The test definition names the risk a projection carries, but nothing recorded
how far the existing suite already answers it. Adds that per site.

Three of the six reads are never executed. getFinancialLogValidityChangeSet is
replaced by a spy at all nine of its appearances, so the projection line itself
never runs. getFinancialLogAssetPrices is stood in for by a hand-written fake that
reimplements the filtering in TypeScript. hasOrderedOwnershipPath appears in no
spec at all.

Three do run and assert their generated statement, one expectation per projected
column, including one that message never appears in the chart-only path. Dropping
a column there turns the suite red.

None of the six runs against a database. Every spec stubs the boundary, and a mock
cannot observe which columns were requested, so the completeness level is
satisfied nowhere - not even where the statement is asserted. Asserting that a
column appears in a statement is not the same as proving the statement returns
every field the response needs.

That leaves one narrow gap: log.repository.ts:699 carries the projection risk,
serves PUT /log/financial/validity, and is not exercised at all. What surrounds it
is well covered - batching, the audit trail, rejection of fabricated audit
records, the block on changing validity through the generic update path.
The inventory described where we stand but never said where we are going, so
the numbers read as an observation rather than as a work list.

States the target: every read path selects the fields it returns and nothing more.

Makes the coverage requirement binding. An endpoint counts as converted only at
4/4 against the four levels, not at 3/4. A projection that drops a field does not
crash - it answers 200 with a wrong value, and in a service moving money that can
run for weeks unnoticed. Converting without the tests trades a slow query for a
silent defect, which is the worse of the two.

Adds a Tests column recording that state per endpoint, and a rule in CONTRIBUTING
that it is updated in the same pull request that changes the code. An unrecorded
conversion cannot be told apart from an untested one and is treated as untested.
Today the column reads not yet 432 times, n/a 100 times and 0/4 once - the one
converted read path whose projection no test executes.

Adds a Spec column alongside it: 57 endpoints have a spec that names their
controller and calls their handler. Labelled in the legend as what it is, a weak
signal and a lower bound - it says a test touches the endpoint, not that it covers
it, and it misses specs that drive a route over HTTP without naming the handler.
…NG overlap

The base moved three commits while this branch was open, and two of them
matter here.

The client error logging change adds a controller, so the inventory was one route
short. Regenerated against the current base: 534 endpoints, the new one being
POST /log/clientError. It reads nothing from the database, which moves that group
from 97 to 98; the 432 that fetch whole rows are unchanged, and so are the 1105
load sites - the new service contains none.

The cron job inventory change added its own item 5 to the PR checklist and its own
paragraph next to where this branch adds the endpoint rules. CONTRIBUTING now
carries both: the cron item stays at 5, the endpoint item follows as 6, and the
endpoint section sits ahead of the cron section rather than replacing it.

Corrects the count of endpoints hidden from Swagger. It is 296, not 223. The
detection searched a window ahead of the routing decorator, where
@ApiExcludeEndpoint does not sit; it is written after the route and before the
method, so an isolated hidden endpoint following a public one was missed and one
following a hidden one was counted by accident. Now the decorator block of the
route itself is read, and a second, line-based count over the sources agrees.
The endpoint entry sat directly below the cron job entry the base added at the
same position in the same list. Two insertions at one anchor cannot be merged -
git has no way to know the intended order - so the branch stayed in conflict even
though both sides wanted both lines.

Stating the obligation as its own sentence under the list keeps it in the same
place for a reader while moving it clear of the anchor. Verified against the
current base with merge-tree: no conflict, and both the cron entry and the cron
section survive intact alongside the endpoint section.
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