captbaritone / captbaritone/grats
Idea for discussion: Directive implementations
- Dominant language
- JavaScript
- Stars
- 358
- Forks
- 24
- PR merge metrics
- No merged PRs in 30d
Description
So it seems that the most common way to implement directives in graphql is as a transformer via `mapSchema`. This is the recommended approach by both [`yoga`](https://the-guild.dev/graphql/tools/docs/schema-directives) and [`apollo-server`](https://www.apollographql.com/docs/apollo-server/schema/directives#custom-directives).
Almost all directives seem to be implemented like the following:
```typescript
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
/** @gqlDirective on FIELD_DEFINITION | OBJECT */
export function directiveFn({}: {}) {} // grats directive fn
export directiveTransformer(schema: GraphQLSchema) {
// Capture/initialise some state for this schema. For example, a map of types to their directive arguments
const typeDirectiveArgumentMaps: Record[0]> = {};
return mapSchema(schema, {
[MapperKind.TYPE]: type => {
const myDirective = getDirective(schema, type, 'directiveName')?.[0] as
| Parameters[0]
| undefined;
if (myDirective) {
typeDirectiveArgumentMaps[type.name] = myDirective;
}
return undefined;
},
[MapperKind.OBJECT_FIELD]: (
fieldConfig: GraphQLFieldConfig,
_fieldName,
typeName,
) => {
const myDirective =
(getDirective(schema, fieldConfig, 'directiveName')?.[0] as
| Parameters[0]
| undefined) ?? typeDirectiveArgumentMaps[typeName];
if (myDirective) {
const { /* args */ } = myDirective;
// Some logic to modify field config
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = function (source, args, context, info) {
// Logic to modify resolver here
return resolve(source, args, context, info);
};
return fieldConfig;
}
},
});
}
```
Now I thought we could call the directive function from grats once per supported field:
```typescript
import {SchemaMapper} from '@graphql-tools/utils';
// Built-in types
type GratsDirectiveArg =
{
[key in Kind]: [key, ...Parameters],
}[Kind];
type GratsDirectiveReturn = ReturnType;
/** @gqlDirective */
export function directiveFn( // Directive locations can be inferred from this
[mapperKind, ...mapperArgs]: GratsDirectiveArg,
{arg}: {arg: Int}, // This is now the result of calling `getDirective` on the current mapped type
): GratsDirectiveReturn {
// But this loses flexibilty like being able to map over kinds where the directive isn't applicable
switch (mapperKind) {
case MapperKind.OBJECT_FIELD: {
const [fieldConfig, fieldName, typeName, schema] = mapperArgs;
// .. Modify field definition
return fieldConfig; // Type-safe
}
// Etc...
}
}
```
The problem with the above is where to put schema state like `typeDirectiveArgumentMaps`.
Weak maps on the schema object can be used for this, and they should work for all usecases that I've seen so far.
The 2nd problem is mapping over kinds where the directive doesn't apply, which is common for object-only directives.
Alternatively, another signature could be:
```typescript
import {mapSchema} from '@graphql-tools/utils';
/** @gqlDirective */
export function directiveFn(
{arg}: {arg: Int}, // Args always useless
schema: GraphQLSchema
): GraphQLSchema {
// This is basically a transform function. First argument is useless
return mapSchema(schema, { /* mapper logic goes here */ })
}
```
But I dislike this because it's too repetitive, args can't be captured into a type, and default values are kinda useless.
I think the right approach is likely somewhere in the middle: a function that is a transform, that takes schema as its only argument and returns a schema, but allows directive arguments type to be used when calling `getDirective`.
This is a follow up from #166
Contributor guide
Assessment
This issue has not been assessed yet.