Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mapping directives to object fields #99

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profusion/apollo-validation-directives",
"version": "4.1.2",
"version": "4.1.3",
"description": "GraphQL directives to implement field validations in Apollo Server",
"author": "Gustavo Sverzut Barbieri <[email protected]>",
"license": "MIT",
Expand Down
Loading