File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ ---
2+ " @objectstack/rest " : patch
3+ ---
4+
5+ fix(rest): register static data-action routes before the greedy ` :object/:id ` matcher
6+
7+ The REST router matches first-registered-wins with no specificity sorting, but
8+ ` registerDataActionEndpoints ` (which holds ` GET /data/:object/export ` ) ran AFTER
9+ ` registerCrudEndpoints ` (which holds the greedy ` GET /data/:object/:id ` ). A
10+ request to ` GET /data/<object>/export ` was therefore captured by ` :object/:id ` —
11+ ` "export" ` treated as a record id — returning ` 404 RECORD_NOT_FOUND ` instead of
12+ streaming the export. The data-action registration now runs first, mirroring the
13+ existing ` /meta/:type/:name/references ` -before-` /meta/:type/:name ` convention.
14+ Reordering is safe both ways: ` registerDataActionEndpoints ` contains no greedy
15+ 2-segment ` :object/:id ` routes, so it cannot shadow any CRUD literal. A
16+ regression test asserts the export route registers ahead of the get-by-id route.
Original file line number Diff line number Diff line change @@ -1552,10 +1552,22 @@ export class RestServer {
15521552 this . registerReportsEndpoints ( bp ) ;
15531553 this . registerApprovalsEndpoints ( bp ) ;
15541554 this . registerAnalyticsEndpoints ( bp ) ;
1555+ // Data-action routes (e.g. GET /data/:object/export, POST
1556+ // /data/:object/import) use static-literal action segments that
1557+ // MUST be registered BEFORE the greedy GET /data/:object/:id
1558+ // matcher in registerCrudEndpoints — the router is first-match-wins
1559+ // with no specificity sorting (see route-manager.ts), so otherwise
1560+ // a request to `.../export` is captured by `:id` and "export" is
1561+ // treated as a record id (404 RECORD_NOT_FOUND). This mirrors the
1562+ // /meta/:type/:name/references-before-/meta/:type/:name convention.
1563+ // Safe in the other direction too: registerDataActionEndpoints has
1564+ // no greedy 2-segment `:object/:id` routes (only literal actions and
1565+ // deeper `:id/clone`, `:id/shares` paths), so it cannot shadow any
1566+ // CRUD literal.
1567+ this . registerDataActionEndpoints ( bp ) ;
15551568 if ( this . config . api . enableCrud ) {
15561569 this . registerCrudEndpoints ( bp ) ;
15571570 }
1558- this . registerDataActionEndpoints ( bp ) ;
15591571 if ( this . config . api . enableBatch ) {
15601572 this . registerBatchEndpoints ( bp ) ;
15611573 }
Original file line number Diff line number Diff line change @@ -904,6 +904,29 @@ describe('RestServer', () => {
904904 // 1 header + 50 000 rows.
905905 expect ( lines . length ) . toBe ( 50_001 ) ;
906906 } ) ;
907+
908+ // Regression: the router is first-match-wins with no specificity sorting,
909+ // so the static-literal `GET /data/:object/export` route MUST be registered
910+ // BEFORE the greedy `GET /data/:object/:id` matcher. Otherwise a request to
911+ // `/data/<object>/export` is captured by `:id` ("export" treated as a record
912+ // id) and 404s with RECORD_NOT_FOUND instead of streaming the export.
913+ it ( 'registers GET /export ahead of the greedy GET /:object/:id matcher' , ( ) => {
914+ const rest = new RestServer ( server as any , protocol as any ) ;
915+ rest . registerRoutes ( ) ;
916+ const routes = rest . getRoutes ( ) ;
917+
918+ const exportIdx = routes . findIndex (
919+ ( r : any ) => r . method === 'GET' && r . path === '/api/v1/data/:object/export' ,
920+ ) ;
921+ const getByIdIdx = routes . findIndex (
922+ ( r : any ) => r . method === 'GET' && r . path === '/api/v1/data/:object/:id' ,
923+ ) ;
924+
925+ expect ( exportIdx ) . toBeGreaterThanOrEqual ( 0 ) ;
926+ expect ( getByIdIdx ) . toBeGreaterThanOrEqual ( 0 ) ;
927+ // First match wins → the more-specific export route must come first.
928+ expect ( exportIdx ) . toBeLessThan ( getByIdIdx ) ;
929+ } ) ;
907930 } ) ;
908931
909932 // -----------------------------------------------------------------------
You can’t perform that action at this time.
0 commit comments