Skip to content

Commit f45aa1b

Browse files
oliverbutlermichaelangrivera
andauthored
fix: unable to nest fastify route implementations (#351)
* fix: unable to nest fastify route implementations [WIP Breaking] * fix: in a non breaking way * test: ensure we can mix and match direct/implemented routers * refactor: remove logs [skip ci] * refactor: remove whitespace * changeset --------- Co-authored-by: michaelangrivera <mar@michaelangrivera.com>
1 parent 4537b1f commit f45aa1b

3 files changed

Lines changed: 174 additions & 7 deletions

File tree

.changeset/mighty-spoons-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/fastify': patch
3+
---
4+
5+
fix: Type error when combining server contracts with fastify

libs/ts-rest/fastify/src/lib/ts-rest-fastify.spec.ts

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,6 @@ describe('ts-rest-fastify', () => {
308308
const router = s.router(postsContract, {
309309
posts: {
310310
getPost: async ({ params }) => {
311-
console.log(params);
312311
return { status: 200, body: { id: params.postId } };
313312
},
314313
},
@@ -454,4 +453,150 @@ describe('ts-rest-fastify', () => {
454453
.send({});
455454
expect(response.statusCode).toEqual(500);
456455
});
456+
457+
it('should be able to instantiate two routers and combine them together', async () => {
458+
const contractA = c.router({
459+
a: {
460+
method: 'GET',
461+
path: '/a',
462+
responses: {
463+
200: z.object({
464+
a: z.string(),
465+
}),
466+
},
467+
},
468+
});
469+
470+
const contractB = c.router({
471+
b: {
472+
method: 'GET',
473+
path: '/b',
474+
responses: {
475+
200: z.object({
476+
b: z.string(),
477+
}),
478+
},
479+
},
480+
});
481+
482+
const combinedContract = c.router({
483+
apiA: contractA,
484+
apiB: contractB,
485+
});
486+
487+
const routerA = s.router(contractA, {
488+
a: async () => {
489+
return {
490+
status: 200,
491+
body: {
492+
a: 'return',
493+
},
494+
};
495+
},
496+
});
497+
498+
const routerB = s.router(contractB, {
499+
b: async () => {
500+
return {
501+
status: 200,
502+
body: {
503+
b: 'return',
504+
},
505+
};
506+
},
507+
});
508+
509+
const combinedRouter = s.router(combinedContract, {
510+
apiA: routerA,
511+
apiB: routerB,
512+
});
513+
514+
const app = fastify({ logger: false });
515+
516+
app.register(s.plugin(combinedRouter));
517+
518+
await app.ready();
519+
520+
const responseA = await supertest(app.server).get('/a');
521+
expect(responseA.statusCode).toEqual(200);
522+
expect(responseA.body).toEqual({
523+
a: 'return',
524+
});
525+
526+
const responseB = await supertest(app.server).get('/b');
527+
expect(responseB.statusCode).toEqual(200);
528+
expect(responseB.body).toEqual({
529+
b: 'return',
530+
});
531+
});
532+
533+
it('should be able to mix and match combining routers with direct implementation', async () => {
534+
const contract = c.router({
535+
apiA: {
536+
a: {
537+
method: 'GET',
538+
path: '/a',
539+
responses: {
540+
200: z.object({
541+
a: z.string(),
542+
}),
543+
},
544+
},
545+
},
546+
apiB: {
547+
b: {
548+
method: 'GET',
549+
path: '/b',
550+
responses: {
551+
200: z.object({
552+
b: z.string(),
553+
}),
554+
},
555+
},
556+
},
557+
});
558+
559+
const routerForApiA = s.router(contract.apiA, {
560+
a: async () => {
561+
return {
562+
status: 200,
563+
body: {
564+
a: 'return',
565+
},
566+
};
567+
},
568+
});
569+
570+
const router = s.router(contract, {
571+
apiA: routerForApiA,
572+
apiB: {
573+
b: async () => {
574+
return {
575+
status: 200,
576+
body: {
577+
b: 'return',
578+
},
579+
};
580+
},
581+
},
582+
});
583+
584+
const app = fastify({ logger: false });
585+
586+
app.register(s.plugin(router));
587+
588+
await app.ready();
589+
590+
const responseA = await supertest(app.server).get('/a');
591+
expect(responseA.statusCode).toEqual(200);
592+
expect(responseA.body).toEqual({
593+
a: 'return',
594+
});
595+
596+
const responseB = await supertest(app.server).get('/b');
597+
expect(responseB.statusCode).toEqual(200);
598+
expect(responseB.body).toEqual({
599+
b: 'return',
600+
});
601+
});
457602
});

libs/ts-rest/fastify/src/lib/ts-rest-fastify.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type AppRouteImplementation<T extends AppRoute> = (
3333

3434
type RecursiveRouterObj<T extends AppRouter> = {
3535
[TKey in keyof T]: T[TKey] extends AppRouter
36-
? RecursiveRouterObj<T[TKey]>
36+
? InitialisedRouter<T[TKey]> | RecursiveRouterObj<T[TKey]>
3737
: T[TKey] extends AppRoute
3838
? AppRouteImplementation<T[TKey]>
3939
: never;
@@ -183,7 +183,6 @@ const requestValidationErrorHandler = (
183183
};
184184

185185
/**
186-
*
187186
* @param routeImpl - User's implementation of the route
188187
* @param appRoute - the `ts-rest` contract for this route (e.g. with Path, params, query, body, etc.)
189188
* @param fastify - the fastify instance to register the route on
@@ -248,6 +247,12 @@ const registerRoute = <TAppRoute extends AppRoute>(
248247
});
249248
};
250249

250+
const implementationIsInitialisedRouter = <T extends AppRouter>(
251+
implementation: InitialisedRouter<T> | RecursiveRouterObj<T>
252+
): implementation is InitialisedRouter<T> => {
253+
return 'contract' in implementation && 'routes' in implementation;
254+
};
255+
251256
/**
252257
*
253258
* @param routerImpl - the user's implementation of the router
@@ -264,14 +269,26 @@ const recursivelyRegisterRouter = <T extends AppRouter>(
264269
options: RegisterRouterOptions
265270
) => {
266271
if (typeof routerImpl === 'object') {
267-
for (const key in routerImpl) {
272+
console.log(routerImpl);
273+
274+
if (implementationIsInitialisedRouter(routerImpl)) {
268275
recursivelyRegisterRouter(
269-
routerImpl[key] as unknown as RecursiveRouterObj<T>,
270-
appRouter[key] as unknown as T,
271-
[...path, key],
276+
routerImpl.routes,
277+
routerImpl.contract,
278+
[...path],
272279
fastify,
273280
options
274281
);
282+
} else {
283+
for (const key in routerImpl) {
284+
recursivelyRegisterRouter(
285+
routerImpl[key] as unknown as RecursiveRouterObj<T>,
286+
appRouter[key] as unknown as T,
287+
[...path, key],
288+
fastify,
289+
options
290+
);
291+
}
275292
}
276293
} else if (typeof routerImpl === 'function') {
277294
registerRoute(

0 commit comments

Comments
 (0)