Skip to content

Commit 580c0d2

Browse files
committed
✨ (README.md): Add documentation for helper utilities and examples of their usage
♻️ (tsconfig.json): update tsconfig to enable declaration output and improve module resolution
1 parent a461927 commit 580c0d2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1268
-2710
lines changed

README.md

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,101 @@ Default scalar mappings:
163163

164164
> You can see a example on [custom-naming](examples/custom-naming)
165165
166+
## Helper Utilities
167+
168+
The plugin provides powerful helper utilities to make your resolver implementation easier and more efficient:
169+
170+
### Available Helpers
171+
172+
```typescript
173+
import {
174+
buildConnection, // Create Relay-compatible connections
175+
buildFilter, // Process filter input arguments
176+
buildSort, // Process sort input arguments
177+
buildSelect, // Create Prisma select objects based on GraphQL fields
178+
QueryBuilder, // Comprehensive query builder
179+
} from '@hakutakuai/zenstack-graphql/helpers'
180+
```
181+
182+
### Select Definitions Pattern
183+
184+
For efficient and type-safe resolvers, you can define reusable select objects:
185+
186+
```typescript
187+
// src/select-definitions.ts
188+
import type { Prisma } from '@prisma/client'
189+
190+
// Base select objects for each model
191+
export const USER_BASE_SELECT = {
192+
id: true,
193+
name: true,
194+
email: true,
195+
} satisfies Prisma.UserSelect
196+
197+
// Select objects with relations
198+
export const USER_WITH_POSTS_SELECT = {
199+
...USER_BASE_SELECT,
200+
posts: {
201+
select: POST_BASE_SELECT,
202+
},
203+
} satisfies Prisma.UserSelect
204+
205+
// Type definitions for select return types
206+
export type UserWithPosts = Prisma.UserGetPayload<{
207+
select: typeof USER_WITH_POSTS_SELECT
208+
}>
209+
```
210+
211+
### Complete Example Flow
212+
213+
Here's how to use the helpers in your resolver:
214+
215+
```typescript
216+
import { Resolver, Query, Arg, Info } from 'type-graphql'
217+
import { GraphQLResolveInfo } from 'graphql'
218+
import { buildConnection, buildFilter, buildSort, buildSelect } from '@hakutakuai/zenstack-graphql/helpers'
219+
import { USER_WITH_POSTS_SELECT } from '../select-definitions'
220+
221+
@Resolver()
222+
export class UserResolver {
223+
@Query(() => UserConnection)
224+
async users(
225+
// Pagination arguments
226+
@Arg('first', () => Int, { nullable: true }) first: number | undefined,
227+
@Arg('after', () => String, { nullable: true }) after: string | undefined,
228+
@Arg('last', () => Int, { nullable: true }) last: number | undefined,
229+
@Arg('before', () => String, { nullable: true }) before: string | undefined,
230+
// Filter and sort arguments
231+
@Arg('filter', () => UserFilterInput, { nullable: true }) filter: UserFilterInput | undefined,
232+
@Arg('sort', () => UserSortInput, { nullable: true }) sort: UserSortInput | undefined,
233+
// GraphQL info for field selection
234+
@Info() info: GraphQLResolveInfo,
235+
@Ctx() { prisma }: Context,
236+
): Promise<UserConnection> {
237+
// Build Prisma arguments
238+
const where = buildFilter(filter)
239+
const orderBy = buildSort(sort)
240+
const select = buildSelect(USER_WITH_POSTS_SELECT, info)
241+
242+
// Create connection config
243+
const config = buildConnection({
244+
first,
245+
after,
246+
last,
247+
before,
248+
where,
249+
orderBy,
250+
select,
251+
})
252+
253+
// Execute query and build connection response
254+
const [items, totalCount] = await Promise.all([prisma.user.findMany(config.findMany), prisma.user.count(config.count)])
255+
256+
return config.toConnection(items, totalCount) as UserConnection
257+
}
258+
}
259+
```
260+
166261
## How It Works
167262

168263
The plugin transforms your ZModel schema into a GraphQL schema through these steps:
@@ -173,6 +268,7 @@ The plugin transforms your ZModel schema into a GraphQL schema through these ste
173268
4. **Connection Generation**: Builds Relay-compatible pagination support
174269
5. **Schema Assembly**: Puts everything together into a complete GraphQL schema
175270
6. **Output**: Writes the final schema to your specified location
271+
7. **Helper Generation**: Provides utility functions for working with the generated schema
176272

177273
> All this happens automatically when you run `zenstack generate`, you can see more about on [Zenstack Plugin System](https://zenstack.dev/docs/the-complete-guide/part2/)!
178274
@@ -398,6 +494,257 @@ export class Post {
398494

399495
These classes are ready to use with TypeGraphQL resolvers and provide full type safety for your GraphQL API.
400496

497+
### Resolver Implementation with Helpers
498+
499+
With the generated classes, you can create efficient resolvers using the provided helper utilities:
500+
501+
```typescript
502+
import { Resolver, Query, Mutation, Arg, ID, Info, Ctx } from 'type-graphql'
503+
import { GraphQLResolveInfo } from 'graphql'
504+
import { User, UserConnection, UserFilterInput, UserSortInput } from '../schema'
505+
import { buildConnection, buildFilter, buildSort, buildSelect } from '@hakutakuai/zenstack-graphql/helpers'
506+
import { USER_WITH_POSTS_SELECT } from '../select-definitions'
507+
508+
@Resolver(() => User)
509+
export class UserResolver {
510+
// Query with pagination, filtering, and sorting
511+
@Query(() => UserConnection)
512+
async users(
513+
@Arg('filter', () => UserFilterInput, { nullable: true }) filter: UserFilterInput | undefined,
514+
@Arg('sort', () => UserSortInput, { nullable: true }) sort: UserSortInput | undefined,
515+
@Arg('first', () => Int, { nullable: true }) first: number | undefined,
516+
@Arg('after', () => String, { nullable: true }) after: string | undefined,
517+
@Arg('last', () => Int, { nullable: true }) last: number | undefined,
518+
@Arg('before', () => String, { nullable: true }) before: string | undefined,
519+
@Info() info: GraphQLResolveInfo,
520+
@Ctx() { prisma }: Context,
521+
): Promise<UserConnection> {
522+
// Convert GraphQL filter to Prisma where
523+
const where = buildFilter(filter as any)
524+
525+
// Convert GraphQL sort to Prisma orderBy
526+
const orderBy = buildSort(sort as any)
527+
528+
// Get Prisma select based on requested GraphQL fields
529+
const select = buildSelect(USER_WITH_POSTS_SELECT, info)
530+
531+
// Build connection config for pagination
532+
const config = buildConnection({
533+
first,
534+
after,
535+
last,
536+
before,
537+
where,
538+
orderBy,
539+
select,
540+
})
541+
542+
// Execute the query with pagination
543+
const [items, totalCount] = await Promise.all([prisma.user.findMany(config.findMany), prisma.user.count(config.count)])
544+
545+
// Convert results to a Connection format
546+
return config.toConnection(items, totalCount) as UserConnection
547+
}
548+
}
549+
```
550+
551+
## Helper Utilities Reference
552+
553+
Here's a comprehensive reference of the helper functions:
554+
555+
### `buildConnection`
556+
557+
Creates a Relay-compatible connection for pagination with cursor support.
558+
559+
```typescript
560+
const config = buildConnection({
561+
// Pagination params
562+
first?: number; // Number of items to fetch
563+
after?: string; // Cursor to start after
564+
last?: number; // Number of items from the end
565+
before?: string; // Cursor to end before
566+
567+
// Optional Prisma params
568+
where?: any; // Prisma where condition
569+
orderBy?: any; // Prisma ordering
570+
select?: any; // Prisma field selection
571+
include?: any; // Prisma include relations
572+
573+
// Options
574+
defaultPageSize?: number; // Default page size
575+
maxPageSize?: number; // Maximum allowed page size
576+
})
577+
578+
// Returns an object with:
579+
// - findMany: Prisma args for fetching items
580+
// - count: Prisma args for counting total items
581+
// - toConnection: Function to convert results to a Connection
582+
```
583+
584+
### `buildFilter`
585+
586+
Converts GraphQL filter input to Prisma where conditions.
587+
588+
```typescript
589+
const where = buildFilter(filterInput)
590+
```
591+
592+
### `buildSort`
593+
594+
Converts GraphQL sort input to Prisma orderBy.
595+
596+
```typescript
597+
const orderBy = buildSort(sortInput)
598+
```
599+
600+
### `buildSelect`
601+
602+
Creates a Prisma select object based on requested GraphQL fields.
603+
604+
```typescript
605+
const select = buildSelect(baseSelect, graphqlInfo)
606+
```
607+
608+
### `QueryBuilder`
609+
610+
A comprehensive builder for constructing complex Prisma queries.
611+
612+
```typescript
613+
import { QueryBuilder } from '@hakutakuai/zenstack-graphql/helpers'
614+
615+
const query = new QueryBuilder().filter(filterInput).sort(sortInput).paginate({ first, after, last, before }).select(baseSelect, graphqlInfo).build()
616+
617+
const results = await prisma.model.findMany(query)
618+
```
619+
620+
## Complete Server Setup Example
621+
622+
Below is a complete example of setting up a GraphQL server using the generated TypeGraphQL classes and helpers:
623+
624+
```typescript
625+
// src/server.ts
626+
import 'reflect-metadata'
627+
import { createYoga } from 'graphql-yoga'
628+
import { buildSchema } from 'type-graphql'
629+
import { createServer } from 'node:http'
630+
import { PrismaClient } from '@prisma/client'
631+
import { resolvers } from './resolvers'
632+
633+
async function main() {
634+
// Create Prisma client
635+
const prisma = new PrismaClient()
636+
637+
// Create TypeGraphQL schema
638+
const schema = await buildSchema({
639+
resolvers,
640+
validate: false,
641+
emitSchemaFile: './generated-schema.graphql',
642+
})
643+
644+
// Create Yoga server
645+
const yoga = createYoga({
646+
schema,
647+
context: { prisma }, // Make Prisma available in resolvers
648+
})
649+
650+
// Create HTTP server
651+
const server = createServer(yoga)
652+
653+
// Start server
654+
server.listen(4000, () => {
655+
console.log('Server is running on http://localhost:4000/graphql')
656+
})
657+
}
658+
659+
main().catch(console.error)
660+
```
661+
662+
```typescript
663+
// src/resolvers/index.ts
664+
import { UserResolver } from './user.resolver'
665+
import { PostResolver } from './post.resolver'
666+
// Import other resolvers...
667+
668+
// Export all resolvers for TypeGraphQL schema building
669+
export const resolvers = [
670+
UserResolver,
671+
PostResolver,
672+
// Add other resolvers...
673+
] as const
674+
```
675+
676+
## Best Practices
677+
678+
Here are some best practices for using the ZenStack GraphQL plugin:
679+
680+
### 1. Organize Select Definitions
681+
682+
Keep your select definitions in a separate file and organize them by model. This makes it easier to reuse select objects across resolvers.
683+
684+
```typescript
685+
// src/select-definitions.ts
686+
import type { Prisma } from '@prisma/client'
687+
688+
// Group by model
689+
export const USER_BASE_SELECT = {...}
690+
export const USER_WITH_POSTS_SELECT = {...}
691+
692+
export const POST_BASE_SELECT = {...}
693+
export const POST_WITH_COMMENTS_SELECT = {...}
694+
```
695+
696+
### 2. Use TypedPayloads for Return Types
697+
698+
Define types for your select objects to ensure type safety in your resolvers:
699+
700+
```typescript
701+
export type UserWithPosts = Prisma.UserGetPayload<{
702+
select: typeof USER_WITH_POSTS_SELECT
703+
}>
704+
705+
// In your resolver:
706+
async function getUser(): Promise<UserWithPosts> {
707+
return prisma.user.findUnique({...}) as UserWithPosts
708+
}
709+
```
710+
711+
### 3. Combine Helpers for Maximum Efficiency
712+
713+
Combine multiple helpers to handle complex query requirements:
714+
715+
```typescript
716+
const where = buildFilter(filter)
717+
const orderBy = buildSort(sort)
718+
const select = buildSelect(baseSelect, info)
719+
const config = buildConnection({ where, orderBy, select, first, after })
720+
```
721+
722+
### 4. Use QueryBuilder for Complex Queries
723+
724+
For more complex queries, use the QueryBuilder to construct your Prisma arguments:
725+
726+
```typescript
727+
const query = new QueryBuilder().filter(filter).sort(sort).paginate({ first, after }).select(baseSelect, info).build()
728+
729+
const results = await prisma.model.findMany(query)
730+
```
731+
732+
### 5. Handle Errors Gracefully
733+
734+
Add error handling to your resolvers to provide meaningful feedback:
735+
736+
```typescript
737+
@Query(() => UserConnection)
738+
async users(@Ctx() ctx: Context): Promise<UserConnection> {
739+
try {
740+
// Your implementation
741+
} catch (error) {
742+
console.error('Failed to fetch users:', error)
743+
throw new Error('Failed to fetch users. Please try again later.')
744+
}
745+
}
746+
```
747+
401748
> Check the [examples](./examples/) directory for more sample use cases!
402749
403750
## Contributing

examples/advanced-graphql/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"setup": "bun install && bun run generate && bun run db:migrate"
1212
},
1313
"dependencies": {
14+
"@hakutakuai/zenstack-graphql": "file:../../dist",
1415
"@prisma/client": "^6.14.0",
1516
"@zenstackhq/runtime": "^2.18.1",
1617
"graphql": "^16.11.0",

0 commit comments

Comments
 (0)