Closed digoburigo closed 1 year ago
Thanks for filing this @digoburigo .
I just briefly checked trpc-openapi and will need to look deeper into how to integrate. Meanwhile, is it more convenient to have a native restful support? ZenStack already has a next.js integration which exposes Primsa CRUD operations as rest-like api endpoints (with Prima query syntax as payload), and integrating with another server should be easy to achieve.
What server are you targeting now?
The goal is to expose REST endpoints for external third party clients, but in all internal tools use only tRPC. The plugin for next is good, but the flexibilty of trpc-openapi it's better for my case.
The server that i'm going to use it's fastify
Got it. Thanks for the information. Let me research a bit more and update back here soon.
Ok. Thanks for your time!
Hey @digoburigo ,
I've been thinking about this and wanted to check for your thoughts on the direction. Right now the trpc routers generated simply mirrors Prisma API and doesn't look RESTful. So I'm wondering if it makes sense to expose them directly as openapi.
Alternatively, we can head the direction where you manually define the trpc routers and corresponding openapi meta, and use the zenstack-enhanced Prisma client for automatic authorization. This way you can have flexibility on how the input and output are shaped in the apis.
E.g.:
Zmodel
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
// policies
@@allow('create,read', true)
}
model Post {
id String @id @default(cuid())
title String
owner User @relation(fields: [ownerId], references: [id])
ownerId String
// policies
@@allow('all', auth() == owner)
}
TRPC Setup
const createContext = ({
req,
res,
}: trpcExpress.CreateExpressContextOptions) => {
// replace this with logic for fetching the user from req
const user = req.headers.authorization
? { id: req.headers.authorization }
: undefined;
return {
req,
res,
prisma: withPresets(prisma, { user }), // put an authorization-enabled prisma into context
};
};
type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.meta<OpenApiMeta>().context<Context>().create();
const router = t.router;
// TODO: generated by zenstack in the future
const zodUser = z.object({
id: z.string(),
email: z.string().email(),
});
const zodPost = z.object({
id: z.string(),
title: z.string(),
ownerId: z.string(),
});
const userRouter = router({
createUser: t.procedure
.meta({ openapi: { method: 'POST', path: '/users' } })
.input(UserSchema.create) // UserSchema is generated by @zenstackhq/trpc plugin
.output(zodUser)
.mutation(async ({ input, ctx }) => {
const user = await ctx.prisma.user.create(input);
return user;
}),
});
const postRouter = router({
createPost: t.procedure
.meta({ openapi: { method: 'POST', path: '/posts' } })
.input(PostSchema.create) // PostSchema is generated by @zenstackhq/trpc plugin
.output(zodPost)
.mutation(async ({ input, ctx }) => {
// only creating post for the current user is allowed
const post = await ctx.prisma.post.create(input);
return post;
t;
}),
listPosts: t.procedure
.meta({ openapi: { method: 'GET', path: '/posts' } })
.input(z.object({}))
.output(z.object({ data: z.array(zodPost) }))
// only posts of the current user are returned
.query(async ({ ctx }) => ({ data: await ctx.prisma.post.findMany() })),
});
Does this make sense to you? The only missing part for this approach is that today zod schemas for the entity types (User, Post) are not generated. This wasn't a problem for regular trpc usage, but trpc-openapi requires an explicit output schema. I can add a separate "@zenstackhq/zod" plugin dedicated to schema generation decoupled with trpc.
Let me know your thoughts.
Yeah. Making a router manually is a great thing for flexibility. The permissions from the zenstack-enhanced Prisma and the generated outputs zod schemas it's really neat in this flow.
But I have one question. In the generated tRPC methods, could I just set a flag or a comment in a tRPC method, so that when regenerating methods they don't get overwritten? If so, I would just set the trpc-openapi
stuff in the generated methods and that's it. What do you thing about this?
Thanks for your response!
Got it. I'm just feeling that partly preserving content during regeneration can be complicated and fragile.
We can also consider supporting trpc-openapi
natively, given its popularity, like:
model Post {
id String @id @default(cuid())
title String
owner User @relation(fields: [ownerId], references: [id])
ownerId String
@@trpc.openApiMeta
}
The @@trpc.openApiMeta
triggers the generation of .meta()
call with fixed rules for method
, path
, summary
, etc. Do you need any customization ability for such properties?
I'm just feeling that partly preserving content during regeneration can be complicated and fragile.
Yeah. I agree with that too.
@@trpc.openApiMeta
Wow. if this is possible would be the ideal implementation I think.
Would be nice to have the customization aspect too.
If none of the properties is given in @@trpc.openApiMeta
would generate a default one, but if you pass some props would use the ones you passed.
The only thing I'm not grasping is how you would customize each CRUD RESTfull endpoint created by this flag, maybe and array of config?
First iteration that I thought
model Post {
id String @id @default(cuid())
title String
owner User @relation(fields: [ownerId], references: [id])
ownerId String
@@trpc.openApiMeta({
create: {
method: "POST",
path: "post",
...
},
update: {
method: "PUT", // parcial update with PATCH?
path: "post",
...
}
})
}
This would be really cool to use though
Unfortunately, attribute argument doesn't support objects yet 😂. Probably time to extend it ...
I feel every prisma CRUD method has a clear corresponding HTTP verb. Even for "update", from Prisma perspective it's always a partial update, so we can fix it to "PATCH".
Here's the mapping:
find*/count/aggregate/groupBy: GET create/createMany/upsert: POST update/updateMany: PATCH delete/deleteMany: DELETE
I guess I'm overthinking. For the generated routers the default with @@trpc.openApiMeta
is already good. If I need a customization I would make it in a custom router anyway, so I don't know if the customization is necessary.
OBS: There is some fields that probably would need customization though, like 'description' for example. So it's a tricky one :)
I slept on it and have a new idea 😄. It's probably cleaner to separate the support to trpc and openapi. We can implement a separate "openapi" plugin to generate openapi spec (V3 only?). You can load that into swagger-ui to get documentation. The "openapi" plugin should given you fine-grained settings for top-level and path-level generation control, close to what you proposed previously with the object syntax. I think then it should have parity with trpc-openapi
in terms of documentation generation.
plugin openapi {
provider = '@zenstackhq/openapi'
output = 'src/openapi/schema.json'
title = 'My API'
version = '1.0.0'
...
}
model Post {
@@openapi.spec({
create: {
method: "POST",
path: "post",
...
}
...
})
}
For the RESTful service, we can provide a fastify adapter to serve CRUD operations, like:
import { requestHandler } from '@zenstackhq/fastify';
import { withPresets } from '@zenstackhq/runtime';
import prisma from './db';
// trpc server
fastify.register(fastifyTRPCPlugin, {
prefix: '/api/trpc',
trpcOptions: { router: appRouter, createContext },
});
// restful server
fastify.route(requestHandler({
endpoint: '/api/openapi',
getPrisma: async (request, reply) => withPresets(prisma, getAuthUser(request))
}));
Since zmodel is the single source of truth, running two separate API services doesn't incur additional development costs. There's also no overhead and complexity brought by trpc-openapi
anymore. You can continue using trpc-openapi
for other manually implemented trpc routers and have them served under separate API endpoints. Just no need to do that for database crud anymore.
How does this sound to you?
Oh, nice. So it would create pure RESTful endpoints without trpc? That's neat, because decouple the logic from tRPC and I can have different things in REST if I want to. That's the best flexibility I think.
Yeah, for me V3 only it's good because I don't have the requirement to maintain old versions.
And like you said, if I want trpc-openapi
I have that separate option too
Right. Pure restful without trpc. In fact the adapter for next.js is almost that, just need some tweaks to make it conform to openapi.
I'll head this direction then. Thanks a lot for your continuously sharing thoughts. I think we can expect the first working version to land in a few days.
resolved by #315
Is your feature request related to a problem? Please describe. I want to have some tRPC methods exposed as RESTful endpoints
Describe the solution you'd like Integration with the trpc-openapi
Describe alternatives you've considered The alternative would be when regenerating the tRPC methods don't overwrite them, so that my changes based on
trpc-openapi
stay thereAdditional context I don't know how authentication and validation would work with
trpc-openapi
OBS: the types in
meta
method fromtrpc-openapi
on the generated procedures from don't work properly