Skip to content

feat(crud): slice 3 REST list delegates to service via listService - #225

Open
JayDS22 wants to merge 1 commit into
accordproject:mainfrom
JayDS22:jay/feat/slice3-rest-list-unification
Open

feat(crud): slice 3 REST list delegates to service via listService#225
JayDS22 wants to merge 1 commit into
accordproject:mainfrom
JayDS22:jay/feat/slice3-rest-list-unification

Conversation

@JayDS22

@JayDS22 JayDS22 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

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.

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: 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.
  • server/services/agreementService.ts: new listAgreementsPaged with the same shape.
  • 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.
  • server/handlers/templates.ts, agreements.ts: pass listService so 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:

  1. feat(errors): standardize MCP JSON-RPC codes into -32020..-32099 range #223 - JSON-RPC error range (merge first)
  2. feat(mcp): port subscriptions/listen SEP-2575 preview upstream from POC #224 - subscriptions/listen SEP-2575 preview
  3. This PR - slice 3 REST list unification (merge last, closes Proposal Core)

Unlocks

Not in this slice (follow-ups)

Validation

  • npm run build: clean
  • npm test: 11 suites / 172 tests, all pass. templates.test.ts and agreements.test.ts continue to exercise the list route through the new delegation path.

Related

Author Checklist

@niallroche niallroche 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.

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.

@JayDS22
JayDS22 force-pushed the jay/feat/slice3-rest-list-unification branch from e4d921c to ed91aad Compare July 31, 2026 07:07
@JayDS22

JayDS22 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Amended ed91aad addressing all four review notes + the nit:

1. Default orderClause fallback for pagination determinism. Both listTemplatesPaged and listAgreementsPaged now default to asc(Template.id) / asc(Agreement.id) when no orderClause is provided, so limit/offset paging is stable by construction. Caller-supplied orderClause is honoured as before. Documented in the service function bodies (line-comment right above the fallback).

2. Read-skew note added in comment. Called out in both services that count and the row-fetch are separate queries without a shared snapshot, so total and items.length can disagree by a row or two under concurrent writes. Kept the current two-query pattern (acceptable for list endpoints), with the repeatable read transaction escape hatch documented for anyone who ever needs strict consistency.

3. REST/MCP shape divergence documented. The apap://templates / apap://agreements MCP resource handlers still call the pre-slice-3 listTemplates / listAgreements (bare array). Slice 3 delivers the { items, total } primitive that #217 needs; the resource-handler switch to listTemplatesPaged / listAgreementsPaged is a small follow-up on that issue. Called out explicitly in the amendment so a client dev seeing the mixed shape has the reason.

4. Boundary tests added. Seven new tests in templateService.test.ts covering limit=0 (clamped to 1), limit=1000 (clamped to 100), negative offset (clamped to 0), in-range pass-through, default orderClause is applied (pagination determinism), caller-provided orderClause is honoured, and total surfaces from the count query. Same clamp logic in listAgreementsPaged is symmetric; happy to add mirror tests there if you'd prefer both sides pinned.

Nit: sharedmodels inline fallback. Added a TODO(#217) block-comment above the fallback branch in crud.ts noting that the fallback path does NOT get the clamping / default-order / read-skew safeguards, and that once a sharedModelService.ts + listSharedModelsPaged land, the fallback branch can be deleted and listService made required on CrudRouterOptions.

npm test: 11 suites / 185 tests, all pass.

@dselman

dselman commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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>
@JayDS22
JayDS22 force-pushed the jay/feat/slice3-rest-list-unification branch from ed91aad to d08c47e Compare August 6, 2026 05:38
@JayDS22

JayDS22 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Force-pushed d08c47e (was ed91aad). Rewritten against post-#227 main:

Validation: npm run build clean, npm test 10 suites / 159 tests all pass (unchanged from main baseline).

Not in this PR (deliberate, follow-up):

Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer-engaged A maintainer has commented or reviewed this item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants