Skip to content

Commit

Permalink
feat: validateEnums + ensure non-nulls
Browse files Browse the repository at this point in the history
with `validateEnums` option it will fail if it finds a value outside of the enum elements + it will
fail if it encounters a null on a non-null field

fix #2 #7
  • Loading branch information
eturino committed Nov 27, 2019
1 parent 0326b05 commit feaff53
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 19 deletions.
18 changes: 9 additions & 9 deletions src/__tests__/scalar-from-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ const schema = makeExecutableSchema({
});

const querySource = `
query MyQuery {
day
morning
someDay: day
someMorning: morning
}
`;
query MyQuery {
day
morning
someDay: day
someMorning: morning
}
`;

const queryDocument: DocumentNode = gql`
${querySource}
Expand Down Expand Up @@ -147,7 +147,7 @@ describe("scalar returned directly from first level queries", () => {

it("use the scalar resolvers in the schema to parse back", done => {
const link = ApolloLink.from([
withScalars(schema),
withScalars({ schema }),
new ApolloLink(() => {
return Observable.of(response);
})
Expand All @@ -171,7 +171,7 @@ describe("scalar returned directly from first level queries", () => {

it("override the scala resolvers with the custom functions map", done => {
const link = ApolloLink.from([
withScalars(schema, typesMap),
withScalars({ schema, typesMap }),
new ApolloLink(() => {
return Observable.of(response);
})
Expand Down
233 changes: 233 additions & 0 deletions src/__tests__/validate-enums-from-query.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import {
ApolloLink,
DocumentNode,
execute,
getOperationName,
GraphQLRequest,
Observable
} from "apollo-link";
import { graphql } from "graphql";
import gql from "graphql-tag";
import { makeExecutableSchema } from "graphql-tools";
import { withScalars } from "..";

const typeDefs = gql`
type Query {
first: MyEnum
second: MyEnum!
third: MyEnum
}
enum MyEnum {
a
b
c
}
`;

const resolvers = {
Query: {
first: () => "a",
second: () => "b",
third: () => null
}
};

const schema = makeExecutableSchema({
typeDefs,
resolvers
});

const querySource = `
query MyQuery {
first
second
third
otherFirst: first
otherSecond: second
otherThird: third
}
`;

const queryDocument: DocumentNode = gql`
${querySource}
`;
const queryOperationName = getOperationName(queryDocument);
if (!queryOperationName) throw new Error("invalid query operation name");

const request: GraphQLRequest = {
query: queryDocument,
variables: {},
operationName: queryOperationName
};

const validResponse = {
data: {
first: "a",
second: "b",
third: null,
otherFirst: "a",
otherSecond: "b",
otherThird: null
}
};

const invalidResponse = {
data: {
first: "a",
second: "b",
third: null,
otherFirst: "invalid",
otherSecond: "b",
otherThird: null
}
};

const badNullsWithAliasResponse = {
data: {
first: null,
second: "b",
third: null,
otherFirst: null,
otherSecond: null,
otherThird: null
}
};
const badNullsResponse = {
data: {
first: null,
second: null,
third: null,
otherFirst: null,
otherSecond: "b",
otherThird: null
}
};

describe("enum returned directly from first level queries", () => {
it("ensure the response fixture is valid (ensure that in the response we have the RAW, the Server is converting from Date to STRING)", async () => {
expect.assertions(1);
const queryResponse = await graphql(schema, querySource);
expect(queryResponse).toEqual(validResponse);
});

describe("with valid enum values", () => {
it("validateEnums false (or missing) => return response", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: false }),
new ApolloLink(() => {
return Observable.of(validResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual(validResponse);
done();
});
expect.assertions(1);
});

it("validateEnums false (or missing) => return response", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: true }),
new ApolloLink(() => {
return Observable.of(validResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual(validResponse);
done();
});
expect.assertions(1);
});
});

describe("with invalid enum values", () => {
it("validateEnums false (or missing) => return invalid response", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: false }),
new ApolloLink(() => {
return Observable.of(invalidResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual(invalidResponse);
done();
});
expect.assertions(1);
});

it("validateEnums true => return error", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: true }),
new ApolloLink(() => {
return Observable.of(invalidResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual({
errors: [
{
message: `enum "MyEnum" with invalid value`
}
]
});
done();
});
expect.assertions(1);
});
});

describe("null values on non-null field", () => {
it("no alias", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: true }),
new ApolloLink(() => {
return Observable.of(badNullsResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual({
errors: [
{
message: `non-null field "second" with null value`
}
]
});
done();
});
expect.assertions(1);
});

it("with alias", done => {
const link = ApolloLink.from([
withScalars({ schema, validateEnums: true }),
new ApolloLink(() => {
return Observable.of(badNullsWithAliasResponse);
})
]);

const observable = execute(link, request);
observable.subscribe(value => {
expect(value).toEqual({
errors: [
{
message: `non-null field "second" (alias "otherSecond") with null value`
}
]
});
done();
});
expect.assertions(1);
});
});
});
28 changes: 23 additions & 5 deletions src/lib/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { ZenObservable } from "zen-observable-ts";
import { FunctionsMap } from "..";
import { treatResult } from "./treat-result";

export const withScalars = (
schema: GraphQLSchema,
typesMap: FunctionsMap = {}
): ApolloLink => {
export const withScalars = ({
schema,
typesMap = {},
validateEnums = false
}: {
schema: GraphQLSchema;
typesMap?: FunctionsMap;
validateEnums?: boolean;
}): ApolloLink => {
const leafTypesMap = pickBy(schema.getTypeMap(), isLeafType);
const functionsMap: FunctionsMap = { ...leafTypesMap, ...typesMap };

Expand All @@ -22,7 +27,20 @@ export const withScalars = (
try {
sub = forward(operation).subscribe({
next: result => {
observer.next(treatResult(schema, functionsMap, operation, result));
try {
const treated = treatResult(
schema,
functionsMap,
operation,
result,
validateEnums
);

observer.next(treated);
} catch (treatError) {
const errors = result.errors || [];
observer.next({ errors: [...errors, treatError] });
}
},
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
Expand Down

0 comments on commit feaff53

Please sign in to comment.