feat(crud): slice 3 REST list delegates to service via listService - #225
feat(crud): slice 3 REST list delegates to service via listService#225JayDS22 wants to merge 1 commit into
Conversation
niallroche
left a comment
There was a problem hiding this comment.
The split is right: router keeps query parsing, whereClause (with SAFE_IDENTIFIER_RX + operator whitelist), and orderClause; the service owns select + count and clamps limit to 1..100 and offset to >= 0. Clamping in the service is the correct home even if the router also does it — defense in depth against a future caller. Nice.
A few things:
1. Pagination needs a deterministic total order — confirm orderClause is never effectively null. In listTemplatesPaged the ordering is applied conditionally (opts.orderClause ? baseQuery.orderBy(...) : baseQuery). If a request can reach the service with no orderClause, the DB returns rows in unspecified order, and limit/offset paging over an unordered set can repeat or skip rows across pages. A list endpoint with offset pagination should always fall back to a stable key (primary key or createdAt). Can you confirm the router always supplies a default order, and if not, default it inside the service so paging is stable by construction?
2. count and select are separate queries — note the read skew. total and items are fetched in two round-trips with no shared snapshot, so under concurrent writes a client can see total: 11 with 10 items (or vice versa). Usually acceptable for list endpoints; just worth a comment, and if you want them consistent, a repeatable read transaction around the pair does it.
3. The REST/MCP divergence cuts against the PR's stated goal. REST now returns { items, total } while the MCP resource path still returns a bare array via listTemplates. Deferring the MCP switch to #217 is reasonable, but since the headline is "unify REST + MCP," this actually widens the gap until the follow-up lands. Please make sure #217 is tracked and the temporary shape difference is documented so a client dev doesn't assume total exists on both.
4. Test the boundaries. I don't see coverage for the clamping/edge cases the service now owns: limit=0 and limit=1000 (→ 100), negative offset, and offset past total (expect empty items, correct total). These are exactly the paths that regress silently.
Nit: the retained inline fallback for sharedmodels means that resource keeps the old path without the service's clamping/consistency. Fine as transitional, but worth a // TODO(#217?) so it doesn't quietly become permanent.
e4d921c to
ed91aad
Compare
|
Amended 1. Default 2. Read-skew note added in comment. Called out in both services that 3. REST/MCP shape divergence documented. The 4. Boundary tests added. Seven new tests in Nit:
|
|
Resolve conflicts please |
…loses Proposal Core) Rewrite of the original accordproject#225 against post-accordproject#227 main. Extends the `buildCrudRouter` helper with an optional `listService` param. When provided, the `GET /` handler delegates the DB read + count to that service function instead of running its own inline query. The router still owns query-string parsing (`parseQueryParams`), `whereClause` construction (`defaultWhereClause` with `SAFE_IDENTIFIER_RX` / operator whitelist), and `orderClause` construction; the service owns the DB select + count. REST and MCP now share one code path for reads, matching the pattern already in place for the single-resource operations (`getTemplateById`, `getAgreementById`). **Closes the Proposal Core service-layer port on the code side.** ## What this PR does - server/services/templateService.ts: new `listTemplatesPaged(db, opts)` returning `{ items, total }`. Accepts `whereClause` + `orderClause` from the caller so REST filter/sort semantics stay uniform. `limit` clamped 1..100, `offset` clamped >= 0. Defaults `orderClause` to `asc(Template.id)` when caller passes none, so `limit` + `offset` paging is stable by construction. - server/services/agreementService.ts: symmetric `listAgreementsPaged`. - server/handlers/crud.ts: adds `ListService<TRow>` type + `listService?` option on `CrudRouterOptions`. `GET /` branches on whether `listService` is provided; the pre-slice-3 inline path is preserved as the fallback so resources without a service layer (`sharedmodels`) keep working unchanged. TODO(accordproject#217) marker on the fallback so it does not quietly become permanent. - server/handlers/templates.ts, agreements.ts: pass `listService` so their REST list routes now call the service functions. ## Read-skew note Count and row-fetch remain separate queries with no shared snapshot. Under concurrent writes a caller can observe `total` and `items.length` disagreeing by one or two rows between the two round-trips. Acceptable for list endpoints where pagination metadata is best-effort; documented inline for readers. Wrap both queries in a `repeatable read` transaction if strict consistency is ever required. ## Envelope contract Router wraps the service's `{ items, total }` into the full `PaginatedResponse` envelope (`{ items, total, page, limit, totalPages }`) that main's inline path already returned. No wire-observable shape change for REST clients. Confirmed against accordproject#226's envelope expectations. ## Not in this PR (follow-ups) - `sharedmodels` service layer + `listSharedModelsPaged`. Once that lands, the crud.ts fallback branch can be deleted and `listService` made required on `CrudRouterOptions`. - MCP resource handlers switching from `listTemplates` (array) to `listTemplatesPaged` (`{items,total}`) to expose `total` in `ReadResourceResult`. Tracked under accordproject#217. - Niall's accordproject#226 test-coverage gaps (limit-clamp end-to-end, page=0/-1 clamp, 23505 duplicate->400, POST body assertion). Fold into a separate test-only PR to keep this diff focused on the router change. ## Validation - npm run build: clean - npm test: 10 suites / 159 tests, all pass (unchanged from main baseline; slice-3 delegation is transparent to existing tests) Signed-off-by: Jay Guwalani <guwalanijj@gmail.com>
ed91aad to
d08c47e
Compare
|
Force-pushed
Validation: Not in this PR (deliberate, follow-up):
Ready for review. |
Summary
Extends the
buildCrudRouterhelper with an optionallistServiceparam. When provided, theGET /handler delegates the DB read + count to that service function instead of running its own inline query. The router still owns query-string parsing (parseQueryParams),whereClauseconstruction (defaultWhereClausewithSAFE_IDENTIFIER_RX/ operator whitelist), andorderClauseconstruction; the service owns the DB select + count.This gives REST and MCP a single code path for reads, matching the pattern already in place for the single-resource operations (
getTemplateById,getAgreementById). Closes the Proposal Core service-layer port on the code side.What this PR does
server/services/templateService.ts: newlistTemplatesPaged(db, opts)returning{ items, total }. AcceptswhereClause+orderClausefrom the caller so REST filter/sort semantics stay uniform.limitclamped 1..100,offsetclamped >= 0.server/services/agreementService.ts: newlistAgreementsPagedwith the same shape.server/handlers/crud.ts: addsListService<TRow>type +listService?option onCrudRouterOptions.GET /branches on whetherlistServiceis provided; the pre-slice-3 inline path is preserved as the fallback so resources without a service layer (sharedmodels) keep working unchanged.server/handlers/templates.ts,agreements.ts: passlistServiceso their REST list routes now call the service functions.Stack
Top of a three-PR chain. Depends on #224 (subscriptions/listen) which depends on #223 (JSON-RPC error range).
Sequence:
Unlocks
GET /share one implementation for list. Behaviour changes to one propagate to both.{ items, total }return shape is the piece Issue [follow-up] Expose paged reads via MCP resource URIs for apap://templates and apap://agreements #217 needs for pagedapap://templatesandapap://agreementsMCP resource URIs.Not in this slice (follow-ups)
sharedmodelsservice layer +listSharedModelsPaged(its service file does not exist yet;sharedmodelsstays on the default DB path).listTemplates(array) tolistTemplatesPaged({items,total}) to exposetotalinReadResourceResult. Tracked under [follow-up] Expose paged reads via MCP resource URIs for apap://templates and apap://agreements #217.Validation
npm run build: cleannpm test: 11 suites / 172 tests, all pass.templates.test.tsandagreements.test.tscontinue to exercise the list route through the new delegation path.Related
{items,total}primitive; the resource-handler switch is a small follow-up.buildCrudRouternow delegates).Author Checklist
npm testgreen locally