Skip to content

Commit 520e11c

Browse files
feat: add defineDirective utility for custom GraphQL directives (#21)
* feat: add defineDirective utility for custom GraphQL directives - Implement defineDirective function to create custom directives - Add automatic directive schema generation in _directives.graphql - Support array type arguments in directive definitions - Create comprehensive directive examples: - @auth: Authentication/authorization with roles - @cache: Field-level caching with TTL and scope - @list: Array manipulation (sort, filter, limit) - @Validate: Input validation with various rules - @Transform: String transformations - @deprecated: Enhanced deprecation notices - @Permission: Multi-role/scope permissions with arrays - @Format: Multiple formatting operations with arrays - Integrate directives into both GraphQL Yoga and Apollo Server - Add file watching support for .directive.ts files - Prevent infinite loops with content comparison before writes * fix: simplify type system and remove undefined scalar types - Remove complex type builders and helpers - Keep simple union type with all GraphQL type combinations - Remove directives using undefined scalar types (URL, EmailAddress, etc) - Clean up unnecessary example files - Maintain simple arg() helper for type inference * fix: improve TypeScript types and error handling for defineDirective - Add virtual module type definitions for proper TypeScript support - Fix array type matching in directive schema generation - Add optional chaining to prevent runtime errors - Rename unused variables to follow lint conventions - Ensure proper type compatibility for virtual modules * feat: add custom directives support with auth and role-based authorization * fix: resolve ESLint issues in directive parser - Add braces around case block variable declaration - Use local DirectiveParser instance instead of global export to avoid use-before-define error
1 parent 8b565af commit 520e11c

29 files changed

+1767
-21
lines changed

README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- 🔄 **Hot Reload**: Development mode with automatic schema and resolver updates
3131
- 📦 **Optimized Bundling**: Smart chunking and dynamic imports for production
3232
- 🌐 **Nuxt Integration**: First-class Nuxt.js support with dedicated module
33+
- 🎭 **Custom Directives**: Create reusable GraphQL directives with automatic schema generation
3334
- 🔗 **Multi-Service Support**: Connect to multiple external GraphQL APIs alongside your main server
3435

3536
## 🎯 Used Projects
@@ -195,6 +196,10 @@ server/
195196
├── graphql/
196197
│ ├── schema.graphql # Main schema with scalars and base types
197198
│ ├── hello.resolver.ts # Global resolvers (use named exports)
199+
│ ├── directives/ # Custom GraphQL directives
200+
│ │ ├── auth.directive.ts # Authentication directive
201+
│ │ ├── cache.directive.ts # Caching directive
202+
│ │ └── validate.directive.ts # Validation directive
198203
│ ├── users/
199204
│ │ ├── user.graphql # User schema definitions
200205
│ │ ├── user-queries.resolver.ts # User query resolvers (use named exports)
@@ -629,6 +634,90 @@ export const postTypes = defineType({
629634

630635
</details>
631636

637+
<details>
638+
<summary><strong>defineDirective</strong> - Create custom GraphQL directives</summary>
639+
640+
```ts
641+
import { defineDirective } from 'nitro-graphql/utils/define'
642+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
643+
import { defaultFieldResolver, GraphQLError } from 'graphql'
644+
645+
export const authDirective = defineDirective({
646+
name: 'auth',
647+
locations: ['FIELD_DEFINITION', 'OBJECT'],
648+
args: {
649+
requires: {
650+
type: 'String',
651+
defaultValue: 'USER',
652+
description: 'Required role to access this field',
653+
},
654+
},
655+
description: 'Directive to check authentication and authorization',
656+
transformer: (schema) => {
657+
return mapSchema(schema, {
658+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
659+
const authDirectiveConfig = getDirective(schema, fieldConfig, 'auth')?.[0]
660+
661+
if (authDirectiveConfig) {
662+
const { resolve = defaultFieldResolver } = fieldConfig
663+
664+
fieldConfig.resolve = async function (source, args, context, info) {
665+
if (!context.user) {
666+
throw new GraphQLError('You must be logged in')
667+
}
668+
669+
if (context.user.role !== authDirectiveConfig.requires) {
670+
throw new GraphQLError('Insufficient permissions')
671+
}
672+
673+
return resolve(source, args, context, info)
674+
}
675+
}
676+
677+
return fieldConfig
678+
},
679+
})
680+
},
681+
})
682+
```
683+
684+
**Usage in Schema:**
685+
```graphql
686+
type User {
687+
id: ID!
688+
name: String!
689+
email: String! @auth(requires: "ADMIN")
690+
secretData: String @auth(requires: "SUPER_ADMIN")
691+
}
692+
693+
type Query {
694+
users: [User!]! @auth
695+
adminStats: AdminStats @auth(requires: "ADMIN")
696+
}
697+
```
698+
699+
**Available Argument Types:**
700+
- Basic scalars: `String`, `Int`, `Float`, `Boolean`, `ID`, `JSON`, `DateTime`
701+
- Non-nullable: `String!`, `Int!`, `Float!`, `Boolean!`, `ID!`, `JSON!`, `DateTime!`
702+
- Arrays: `[String]`, `[String!]`, `[String]!`, `[String!]!` (and all combinations for other types)
703+
- Custom types: Any string for your custom GraphQL types
704+
705+
**Helper Function:**
706+
```ts
707+
export const validateDirective = defineDirective({
708+
name: 'validate',
709+
locations: ['FIELD_DEFINITION', 'ARGUMENT_DEFINITION'],
710+
args: {
711+
minLength: arg('Int', { description: 'Minimum length' }),
712+
maxLength: arg('Int', { description: 'Maximum length' }),
713+
pattern: arg('String', { description: 'Regex pattern' }),
714+
},
715+
// ... transformer implementation
716+
})
717+
```
718+
719+
</details>
720+
632721
<details>
633722
<summary><strong>defineSchema</strong> - Define custom schema with validation</summary>
634723

@@ -755,6 +844,75 @@ export default defineNitroConfig({
755844

756845
## 🔥 Advanced Features
757846

847+
<details>
848+
<summary><strong>Custom Directives</strong></summary>
849+
850+
Create reusable GraphQL directives with automatic schema generation:
851+
852+
```ts
853+
// server/graphql/directives/auth.directive.ts
854+
import { defineDirective } from 'nitro-graphql/utils/define'
855+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
856+
857+
export const authDirective = defineDirective({
858+
name: 'auth',
859+
locations: ['FIELD_DEFINITION', 'OBJECT'],
860+
args: {
861+
requires: {
862+
type: 'String',
863+
defaultValue: 'USER',
864+
description: 'Required role to access this field',
865+
},
866+
},
867+
description: 'Authentication and authorization directive',
868+
transformer: (schema) => {
869+
return mapSchema(schema, {
870+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
871+
const authConfig = getDirective(schema, fieldConfig, 'auth')?.[0]
872+
if (authConfig) {
873+
// Transform field resolvers to check authentication
874+
const { resolve = defaultFieldResolver } = fieldConfig
875+
fieldConfig.resolve = async (source, args, context, info) => {
876+
if (!context.user || context.user.role !== authConfig.requires) {
877+
throw new GraphQLError('Access denied')
878+
}
879+
return resolve(source, args, context, info)
880+
}
881+
}
882+
return fieldConfig
883+
},
884+
})
885+
},
886+
})
887+
```
888+
889+
**Common Directive Examples:**
890+
- `@auth(requires: "ADMIN")` - Role-based authentication
891+
- `@cache(ttl: 300, scope: "PUBLIC")` - Field-level caching
892+
- `@rateLimit(limit: 10, window: 60)` - Rate limiting
893+
- `@validate(minLength: 5, maxLength: 100)` - Input validation
894+
- `@transform(upper: true, trim: true)` - Data transformation
895+
- `@permission(roles: ["ADMIN", "MODERATOR"])` - Multi-role permissions
896+
897+
**Usage in Schema:**
898+
```graphql
899+
type User {
900+
id: ID!
901+
name: String!
902+
email: String! @auth(requires: "ADMIN")
903+
posts: [Post!]! @cache(ttl: 300)
904+
}
905+
906+
type Query {
907+
users: [User!]! @rateLimit(limit: 100, window: 3600)
908+
sensitiveData: String @auth(requires: "SUPER_ADMIN")
909+
}
910+
```
911+
912+
The module automatically generates the directive schema definitions and integrates them with both GraphQL Yoga and Apollo Server.
913+
914+
</details>
915+
758916
<details>
759917
<summary><strong>Custom Scalars</strong></summary>
760918

playground-nuxt/server/graphql/data/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ export const users: User[] = [
1616
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', createdAt: new Date('2024-01-03') },
1717
]
1818

19-
console.log('[Data] Users array initialized with', users.length, 'users')
20-
console.log('[Data] Users:', users)
21-
2219
// Utility functions
2320
export const generateId = () => Date.now().toString()
2421

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# WARNING: This file is auto-generated by nitro-graphql
2+
# Do not modify this file directly. It will be overwritten.
3+
# To define custom directives, create .directive.ts files using defineDirective()
4+
5+
directive @auth(requires: String = "USER") on FIELD_DEFINITION
6+
7+
directive @hasRole(role: String!) on FIELD_DEFINITION
8+
9+
directive @cache(ttl: Int = 60, scope: String = "PUBLIC") on FIELD_DEFINITION
10+
11+
directive @deprecatedField(reason: String = "No longer supported", removeAt: String) on FIELD_DEFINITION
12+
13+
directive @format(operations: [String!], dateFormats: [String!], customPatterns: [String!]) on FIELD_DEFINITION
14+
15+
directive @list(max: Int, sort: String, sortDesc: Boolean = false, filter: String, reverse: Boolean = false, unique: String) on FIELD_DEFINITION
16+
17+
directive @permission(roles: [String!], scopes: [String!], requireAll: Boolean = false) on FIELD_DEFINITION | OBJECT
18+
19+
directive @rateLimit(limit: Int!, window: Int! = 60, skipIf: [String!], keyBy: [String!], message: String = "Too many requests", burst: Int, cost: Float = 1) on FIELD_DEFINITION | OBJECT
20+
21+
directive @transform(upper: Boolean, lower: Boolean, trim: Boolean = true, truncate: Int, default: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION
22+
23+
directive @validate(minLength: Int, maxLength: Int, pattern: String, min: Int, max: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION

playground/server/graphql/context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33

44
declare module 'h3' {
55
interface H3EventContext {
6+
event: H3Event
7+
storage: any
8+
user?: {
9+
id: string
10+
name: string
11+
email: string
12+
role: 'USER' | 'ADMIN'
13+
}
614
// Add your custom context properties here
715
// useDatabase: () => Database
816
// tables: typeof import('../drizzle/schema')

playground/server/graphql/data/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ export const users: User[] = [
1616
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', createdAt: new Date('2024-01-03') },
1717
]
1818

19-
console.log('[Data] Users array initialized with', users.length, 'users')
20-
console.log('[Data] Users:', users)
21-
2219
// Utility functions
2320
export const generateId = () => Date.now().toString()
2421

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
2+
import { defaultFieldResolver, GraphQLError } from 'graphql'
3+
4+
export const authDirective = defineDirective({
5+
name: 'auth',
6+
locations: ['FIELD_DEFINITION'],
7+
args: {
8+
requires: {
9+
type: 'String',
10+
defaultValue: 'USER',
11+
description: 'Required role to access this field',
12+
},
13+
},
14+
description: 'Directive to check authentication',
15+
transformer: (schema) => {
16+
return mapSchema(schema, {
17+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
18+
const authDirectiveConfig = getDirective(schema, fieldConfig, 'auth')?.[0]
19+
20+
if (authDirectiveConfig) {
21+
const { resolve = defaultFieldResolver } = fieldConfig
22+
23+
fieldConfig.resolve = async function (source, args, context, info) {
24+
const { auth } = context
25+
const user = auth?.user
26+
27+
if (!user?.id) {
28+
throw new GraphQLError('You must be logged in to access this field', {
29+
extensions: {
30+
code: 'UNAUTHENTICATED',
31+
},
32+
})
33+
}
34+
35+
return resolve(source, args, context, info)
36+
}
37+
}
38+
39+
return fieldConfig
40+
},
41+
})
42+
},
43+
})
44+
45+
export const hasRoleDirective = defineDirective({
46+
name: 'hasRole',
47+
locations: ['FIELD_DEFINITION'],
48+
args: {
49+
role: {
50+
type: 'String!',
51+
description: 'Required role to access this field',
52+
},
53+
},
54+
description: 'Directive to check user role authorization',
55+
transformer: (schema) => {
56+
return mapSchema(schema, {
57+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
58+
const hasRoleDirectiveConfig = getDirective(schema, fieldConfig, 'hasRole')?.[0]
59+
60+
if (hasRoleDirectiveConfig) {
61+
const { role: requiredRole } = hasRoleDirectiveConfig
62+
const { resolve = defaultFieldResolver } = fieldConfig
63+
64+
fieldConfig.resolve = async function (source, args, context, info) {
65+
const { auth } = context
66+
const user = auth?.user
67+
68+
if (!user?.id) {
69+
throw new GraphQLError('You must be logged in to access this field', {
70+
extensions: {
71+
code: 'UNAUTHENTICATED',
72+
},
73+
})
74+
}
75+
76+
const userRole = context.userRole
77+
78+
if (!userRole || userRole !== requiredRole) {
79+
throw new GraphQLError(`You must have ${requiredRole} role to access this field`, {
80+
extensions: {
81+
code: 'FORBIDDEN',
82+
},
83+
})
84+
}
85+
86+
return resolve(source, args, context, info)
87+
}
88+
}
89+
90+
return fieldConfig
91+
},
92+
})
93+
},
94+
})

0 commit comments

Comments
 (0)