-
Notifications
You must be signed in to change notification settings - Fork 1k
/
Copy pathuseRedwoodDirective.ts
186 lines (163 loc) · 5.27 KB
/
useRedwoodDirective.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import { Plugin } from '@envelop/types'
import {
DirectiveNode,
DocumentNode,
getDirectiveValues,
GraphQLObjectType,
GraphQLResolveInfo,
} from 'graphql'
import { GlobalContext } from '../index'
export interface DirectiveParams<FieldType = any> {
root: unknown
args: Record<string, unknown>
context: GlobalContext
info: GraphQLResolveInfo
directiveNode?: DirectiveNode
directiveArgs: Record<string, any>
resolvedValue: FieldType
}
/**
* Write your validation logic inside this function.
* Validator directives do not have access to the field value, i.e. they are called before resolving the value
*
* - Throw an error, if you want to stop executing e.g. not sufficient permissions
* - Validator directives can be async or sync
* - Returned value will be ignored
*/
export type ValidatorDirectiveFunc<FieldType = any> = (
args: Omit<DirectiveParams<FieldType>, 'resolvedValue'>
) => Promise<void> | void
/**
* Write your transformation logic inside this function.
* Transformer directives run **after** resolving the value
*
* - You can also throw an error, if you want to stop executing, but note that the value has already been resolved
* - Transformer directives **must** be synchonous, and return a value
*
*/
export type TransformerDirectiveFunc<FieldType = any> = (
args: DirectiveParams<FieldType>
) => FieldType
// @NOTE don't use unspecified enums, because !type would === true
export enum DirectiveType {
VALIDATOR = 'VALIDATOR_DIRECTIVE',
TRANSFORMER = 'TRANSFORMER_DIRECTIVE',
}
export type RedwoodDirective = ValidatorDirective | TransformerDirective
export interface ValidatorDirective extends ValidatorDirectiveOptions {
schema: DocumentNode
}
export interface TransformerDirective extends TransformerDirectiveOptions {
schema: DocumentNode
}
interface ValidatorDirectiveOptions {
onExecute: ValidatorDirectiveFunc
type: DirectiveType.VALIDATOR
name: string
}
interface TransformerDirectiveOptions {
onExecute: TransformerDirectiveFunc
type: DirectiveType.TRANSFORMER
name: string
}
export type DirectivePluginOptions =
| ValidatorDirectiveOptions
| TransformerDirectiveOptions
export function hasDirective(info: GraphQLResolveInfo): boolean {
try {
const { parentType, fieldName, schema } = info
const schemaType = schema.getType(parentType.name) as GraphQLObjectType
const field = schemaType.getFields()[fieldName]
const astNode = field.astNode
// if directives array exists, we check the length
// other wise false
return !!astNode?.directives?.length
} catch (error) {
console.error(error)
return false
}
}
export function getDirectiveByName(
info: GraphQLResolveInfo,
name: string
): null | DirectiveNode {
try {
const { parentType, fieldName, schema } = info
const schemaType = schema.getType(parentType.name) as GraphQLObjectType
const field = schemaType.getFields()[fieldName]
const astNode = field.astNode
const associatedDirective = astNode?.directives?.find(
(directive) => directive.name.value === name
)
return associatedDirective || null
} catch (error) {
console.error(error)
return null
}
}
export const useRedwoodDirective = (
options: DirectivePluginOptions
): Plugin<{
onExecute: ValidatorDirectiveFunc | TransformerDirectiveFunc
}> => {
return {
onExecute({ args: executionArgs }) {
return {
async onResolverCalled({ args, root, context, info }) {
const directiveNode = getDirectiveByName(info, options.name)
const directive = directiveNode
? executionArgs.schema.getDirective(directiveNode.name.value)
: null
if (directiveNode && directive) {
const directiveArgs =
getDirectiveValues(
directive,
{ directives: [directiveNode] },
executionArgs.variableValues
) || {}
if (_isValidator(options)) {
await options.onExecute({
root,
args,
context,
info,
directiveNode,
directiveArgs,
})
}
// In order to change the value of the field, we have to return a function in this form
// ({result, setResult}) => { setResult(newValue)}
// Not super clear but mentioned here: https://www.envelop.dev/docs/plugins/lifecycle#onexecuteapi
if (_isTransformer(options)) {
return ({ result, setResult }) => {
// @NOTE! A transformer cannot be async
const transformedValue = options.onExecute({
root,
args,
context,
info,
directiveNode,
directiveArgs,
resolvedValue: result,
})
setResult(transformedValue)
}
}
}
return
},
}
},
}
}
// For narrowing types
const _isValidator = (
options: DirectivePluginOptions
): options is ValidatorDirectiveOptions => {
return options.type === DirectiveType.VALIDATOR
}
const _isTransformer = (
options: DirectivePluginOptions
): options is TransformerDirectiveOptions => {
return options.type === DirectiveType.TRANSFORMER
}