Skip to content

Commit

Permalink
fix: mapping directives to object fields
Browse files Browse the repository at this point in the history
  • Loading branch information
felipebergamin committed Dec 5, 2023
1 parent 66d3c88 commit 7c81a11
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 126 deletions.
14 changes: 5 additions & 9 deletions lib/EasyDirectiveVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,22 +355,18 @@ abstract class EasyDirectiveVisitor<
}

// istanbul ignore next (should be overridden and never reached)
public visitQuery(
query: GraphQLObjectType<any, TContext>,
public visitObjectFieldsAndArgumentInputs(
object: GraphQLObjectType<unknown, unknown>,
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<any, TContext> {
): GraphQLObjectType<unknown, unknown> {
throw new Error('Method not implemented.');
}

// istanbul ignore next (should be overridden and never reached)
public visitMutation(
mutation: GraphQLObjectType<any, TContext>,
public addInputTypesValidations(
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<any, TContext> {
throw new Error('Method not implemented.');
}
): void {}
/* eslint-enable class-methods-use-this, class-methods-use-this, @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */

public applyToSchema(schema: GraphQLSchema): GraphQLSchema {
Expand Down
40 changes: 13 additions & 27 deletions lib/ValidateDirectiveVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,10 @@ const visitInputFieldsWithDirective = <
if (directiveArgs) {
// eslint-disable-next-line no-param-reassign
visitor.args = directiveArgs as TArgs;
visitor.visitInputFieldDefinition(field, { objectType: inputObject });
// @ts-expect-error field added by MapperKind.OBJECT_TYPE to avoid wrapping input type validation twice
if (!field.visited) {
visitor.visitInputFieldDefinition(field, { objectType: inputObject });
}
}
});
};
Expand Down Expand Up @@ -1077,44 +1080,27 @@ abstract class ValidateDirectiveVisitor<
});
}

public visitQuery(
query: GraphQLObjectType<unknown, TContext>,
public visitObjectFieldsAndArgumentInputs(
object: GraphQLObjectType<unknown, unknown>,
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<unknown, TContext> {
const queryFields = Object.values(query.getFields());
visitInputObjectsAndFieldsWithDirective(schema, directiveName, this);
): GraphQLObjectType<unknown, unknown> {
const typeFields = Object.values(object.getFields());
visitArgumentsWithDirectiveInObjectFields(
queryFields,
typeFields,
schema,
directiveName,
this,
);
wrapFieldsRequiringValidation(
queryFields,
typeFields,
ValidateDirectiveVisitor.validationErrorsArgumentName,
);
return query;
return object;
}

// eslint-disable-next-line class-methods-use-this
public visitMutation(
mutation: GraphQLObjectType<unknown, TContext>,
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<unknown, TContext> {
const mutationFields = Object.values(mutation.getFields());
visitArgumentsWithDirectiveInObjectFields(
mutationFields,
schema,
directiveName,
this,
);
wrapFieldsRequiringValidation(
mutationFields,
ValidateDirectiveVisitor.validationErrorsArgumentName,
);
return mutation;
addInputTypesValidations(schema: GraphQLSchema, directiveName: string): void {
visitInputObjectsAndFieldsWithDirective(schema, directiveName, this);
}
}

Expand Down
36 changes: 4 additions & 32 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import type {
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLFieldConfig,
GraphQLSchema,
} from 'graphql';

import { getDirective } from '@graphql-tools/utils';

import EasyDirectiveVisitor from './EasyDirectiveVisitor.js';
import AuthenticationError from './errors/AuthenticationError.js';

Expand Down Expand Up @@ -80,35 +77,10 @@ class AuthDirectiveVisitor<
}

// eslint-disable-next-line class-methods-use-this
public visitQuery(
query: GraphQLObjectType<unknown, TContext>,
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<unknown, TContext> {
const fields = Object.values(query.getFields());
fields.forEach(field => {
const [directive] = getDirective(schema, field, directiveName) ?? [];
if (directive) {
this.visitFieldDefinition(field);
}
});

return query;
}

public visitMutation(
query: GraphQLObjectType<unknown, TContext>,
schema: GraphQLSchema,
directiveName: string,
): GraphQLObjectType<unknown, TContext> {
const fields = Object.values(query.getFields());
fields.forEach(field => {
const [directive] = getDirective(schema, field, directiveName) ?? [];
if (directive) {
this.visitFieldDefinition(field);
}
});
return query;
public visitObjectFieldsAndArgumentInputs(
object: GraphQLObjectType<unknown, unknown>,
): GraphQLObjectType<unknown, unknown> {
return object;
}
}

Expand Down
83 changes: 36 additions & 47 deletions lib/createSchemaMapperForVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { SchemaMapper } from '@graphql-tools/utils';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import type {
DirectiveLocation,
GraphQLArgument,
GraphQLFieldConfig,
GraphQLObjectType,
GraphQLSchema,
Expand All @@ -17,53 +16,43 @@ export const createMapper = <T extends DirectiveLocation>(
directiveName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
visitor: EasyDirectiveVisitor<any, any, T>,
): SchemaMapper => ({
[MapperKind.QUERY](query, schema): GraphQLObjectType {
visitor.visitQuery(query, schema, directiveName);
return query;
},
[MapperKind.MUTATION](mutation, schema): GraphQLObjectType {
visitor.visitMutation(mutation, schema, directiveName);
return mutation;
},
[MapperKind.OBJECT_TYPE](type, schema): GraphQLObjectType {
Object.values(type.getFields()).forEach(field => {
field.args.forEach(arg => {
const [directive] = getDirective(schema, arg, directiveName) ?? [];
if (!directive) return;
// eslint-disable-next-line no-param-reassign
visitor.args = directive;
visitor.visitArgumentDefinition(arg as GraphQLArgument, {
field,
): SchemaMapper => {
let haveVisitedInputs = false;
return {
[MapperKind.OBJECT_TYPE](type, schema): GraphQLObjectType {
if (!haveVisitedInputs) {
visitor.addInputTypesValidations(schema, directiveName);
haveVisitedInputs = true;
}
visitor.visitObjectFieldsAndArgumentInputs(type, schema, directiveName);
const [directive] = getDirective(schema, type, directiveName) ?? [];
if (!directive) return type;
// eslint-disable-next-line no-param-reassign
visitor.args = directive;
visitor.visitObject(type);
return type;
},
[MapperKind.OBJECT_FIELD](
fieldConfig,
_fieldName,
typeName,
schema,
): GraphQLFieldConfig<unknown, unknown> {
const [directive] =
getDirective(schema, fieldConfig, directiveName) ?? [];
if (!directive) return fieldConfig;
// eslint-disable-next-line no-param-reassign
visitor.args = directive;
const objectType = schema.getType(typeName);
if (isObjectType(objectType)) {
visitor.visitFieldDefinition(fieldConfig, {
objectType,
});
});
});
const [directive] = getDirective(schema, type, directiveName) ?? [];
if (!directive) return type;
// eslint-disable-next-line no-param-reassign
visitor.args = directive;
visitor.visitObject(type);
return type;
},
[MapperKind.OBJECT_FIELD](
fieldConfig,
_fieldName,
typeName,
schema,
): GraphQLFieldConfig<unknown, unknown> {
const [directive] = getDirective(schema, fieldConfig, directiveName) ?? [];
if (!directive) return fieldConfig;
// eslint-disable-next-line no-param-reassign
visitor.args = directive;
const objectType = schema.getType(typeName);
if (isObjectType(objectType)) {
visitor.visitFieldDefinition(fieldConfig, {
objectType,
});
}
return fieldConfig;
},
});
}
return fieldConfig;
},
};
};

export const createSchemaMapperForVisitor =
<T extends DirectiveLocation>(
Expand Down
73 changes: 73 additions & 0 deletions lib/foreignNodeId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,77 @@ ${validationDirectionEnumTypeDefs(capitalizedName)}
expect(fromNodeIdSpy).toHaveBeenCalledTimes(1);
expect(fromNodeIdSpy).toHaveBeenCalledWith(encodedId);
});

it('should be applied ', async () => {
const typeName = 'MyType';
const decodedIds = ['abc', '123e'];
const encodedIds = decodedIds.map(toNodeId.bind(null, typeName));
const schema = new ForeignNodeId().applyToSchema(
makeExecutableSchema({
resolvers: {
Query: {
users: () => [{}],
},
User: {
dummyField: () => 'hi',
fieldWithInput: (_, { input: { typeIds } }) => typeIds,
},
},
typeDefs: [
...directiveTypeDefs,
gql`
input DummyInput {
field: String
}
input Input1 {
typeIds: [ID!]! @foreignNodeId(typename: "${typeName}")
}
type User {
dummyField(input: DummyInput): String
fieldWithInput(input: Input1!): [String!]!
}
type Query {
users: [User!]!
}
`,
],
}),
);
const source = print(gql`
query Test($input: Input1!) {
users {
dummyField
fieldWithInput(input: $input)
}
}
`);
const contextValue = ForeignNodeId.createDirectiveContext({
fromNodeId,
});
const fromNodeIdSpy = jest.spyOn(contextValue, 'fromNodeId');

const result = await graphql({
contextValue,
schema,
source,
variableValues: {
input: {
typeIds: encodedIds,
},
},
});

expect(fromNodeIdSpy).toHaveBeenCalledTimes(2);
expect(result).toEqual({
data: {
users: [
{
dummyField: 'hi',
fieldWithInput: decodedIds,
},
],
},
error: undefined,
});
});
});
19 changes: 8 additions & 11 deletions lib/hasPermissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1430,29 +1430,26 @@ enum HasPermissionsDirectivePolicy {
public static readonly defaultName: string = 'injectMissingPermissions';

// eslint-disable-next-line class-methods-use-this
public visitQuery(
query: GraphQLObjectType<unknown, unknown>,
): GraphQLObjectType<unknown, unknown> {
const field = query.getFields().test;
public override visitFieldDefinition(
field: GraphQLFieldConfig<unknown, unknown, unknown>,
): GraphQLFieldConfig<unknown, unknown, unknown> {
const { resolve = defaultFieldResolver } = field;
// eslint-disable-next-line no-param-reassign

field.resolve = function (obj, args, context, info): unknown {
const enhancedInfo = {
...info,
missingPermissions: 'This should be an array!',
};
return resolve.apply(this, [obj, args, context, enhancedInfo]);
};

return query;
return field;
}

// eslint-disable-next-line class-methods-use-this
public visitFieldDefinition(
field: GraphQLFieldConfig<unknown, unknown, unknown>,
): GraphQLFieldConfig<unknown, unknown, unknown> {
return field;
public override visitObjectFieldsAndArgumentInputs(
object: GraphQLObjectType<unknown, unknown>,
): GraphQLObjectType<unknown, unknown> {
return object;
}
}

Expand Down

0 comments on commit 7c81a11

Please sign in to comment.