Skip to content
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
109 changes: 109 additions & 0 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,115 @@ describe('ParseGraphQLServer', () => {
expect(introspection.data).toBeDefined();
expect(introspection.data.__type).toBeDefined();
});

it('should strip "Did you mean" field suggestions from validation errors without master or maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).not.toMatch(/Did you mean/);
expect(message).not.toContain('health');
}
});

it('should strip "Did you mean" argument suggestions from validation errors without master or maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query UnknownArg {
users(wher: {}) {
edges {
node {
id
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Unknown argument "wher"');
expect(message).not.toMatch(/Did you mean/);
expect(message).not.toContain('"where"');
}
});

it('should keep "Did you mean" suggestions with master key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});

it('should keep "Did you mean" suggestions with maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
context: {
headers: {
'X-Parse-Maintenance-Key': 'test2',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});

it('should keep "Did you mean" suggestions when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });

try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});
});


Expand Down
29 changes: 28 additions & 1 deletion src/GraphQL/ParseGraphQLServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({

});

// graphql-js validation rules (FieldsOnCorrectTypeRule, KnownArgumentNamesRule,
// KnownTypeNamesRule, ...) embed "Did you mean ...?" hints sourced from the live
// schema in their error messages. Those messages are returned to the caller
// before didResolveOperation runs, so they sidestep IntrospectionControlPlugin
// and disclose schema identifiers the introspection guard is meant to hide.
// Strip the hint suffix for callers that are not allowed to introspect.
const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
requestDidStart: async (requestContext) => ({
validationDidStart: async () => {
if (publicIntrospection) {
return;
}
const isMasterOrMaintenance =
requestContext.contextValue.auth?.isMaster ||
requestContext.contextValue.auth?.isMaintenance;
if (isMasterOrMaintenance) {
return;
}
return async (validationErrors) => {
validationErrors?.forEach(error => {
error.message = error.message.replace(/ ?Did you mean(.+?)\?$/, '');
});
};
},
}),
});

class ParseGraphQLServer {
parseGraphQLController: ParseGraphQLController;

Expand Down Expand Up @@ -153,7 +180,7 @@ class ParseGraphQLServer {
// We need always true introspection because apollo server have changing behavior based on the NODE_ENV variable
// we delegate the introspection control to the IntrospectionControlPlugin
introspection: true,
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
plugins: [ApolloServerPluginCacheControlDisabled(), IntrospectionControlPlugin(this.config.graphQLPublicIntrospection), SchemaSuggestionsControlPlugin(this.config.graphQLPublicIntrospection), createComplexityValidationPlugin(() => this.parseServer.config.requestComplexity)],
schema,
});
await apollo.start();
Expand Down
Loading