Skip to content

Commit 2bf9123

Browse files
authored
fix: custom formatters on arrays (#232)
Fixes #220
1 parent 97f1c29 commit 2bf9123

File tree

6 files changed

+176
-9
lines changed

6 files changed

+176
-9
lines changed

apollo4.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {DocumentNode} from "graphql";
2-
import {ApolloServerPlugin} from '@apollo/server';
1+
import type {DocumentNode} from "graphql";
2+
import type {ApolloServerPlugin, BaseContext} from '@apollo/server';
3+
import type {PluginOptions} from '.';
34

45
/**
56
* Constraint directive typeDef as a `string`
@@ -13,7 +14,7 @@ export const constraintDirectiveTypeDefsGql: DocumentNode;
1314

1415
/**
1516
* Create Apollo 4 validation plugin.
16-
*
17+
*
1718
* @param options to setup plugin.
1819
*/
19-
export function createApollo4QueryValidationPlugin ( options?: {} ) : ApolloServerPlugin;
20+
export function createApollo4QueryValidationPlugin <TContext extends BaseContext>( options?: PluginOptions ) : ApolloServerPlugin<TContext>;

lib/query-validation-visitor.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ module.exports = class QueryValidationVisitor {
137137

138138
validateInputTypeValue(this.context, inputObjectTypeDef, argName, variableName, value, this.currentField, variableName, this.options)
139139
} else if (isListType(valueTypeDef)) {
140-
validateArrayTypeValue(this.context, valueTypeDef, argTypeDef, value, this.currentField, argName, variableName, variableName)
140+
validateArrayTypeValue(this.context, valueTypeDef, argTypeDef, value, this.currentField, argName, variableName, variableName, this.options)
141141
} else {
142142
// nothing to validate
143143
if (!value && value !== '' && value !== 0) return
@@ -184,7 +184,7 @@ function validateInputTypeValue (context, inputObjectTypeDef, argName, variableN
184184
visit(inputObjectTypeDef.astNode, visitor)
185185
}
186186

187-
function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, value, currentField, argName, variableName, iFieldNameFull) {
187+
function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, value, currentField, argName, variableName, iFieldNameFull, options = {}) {
188188
if (!typeDefWithDirective.astNode) { return }
189189

190190
let valueTypeDefArray = valueTypeDef.ofType
@@ -230,11 +230,11 @@ function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, va
230230
const iFieldNameFullIndexed = iFieldNameFull ? `${iFieldNameFull}[${index++}]` : `[${index++}]`
231231

232232
if (isInputObjectType(valueTypeDefArray)) {
233-
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed, this.options)
233+
validateInputTypeValue(context, valueTypeDefArray, argName, variableName, element, currentField, iFieldNameFullIndexed, options)
234234
} else if (hasNonListValidation) {
235235
const atMessage = ` at "${iFieldNameFullIndexed}"`
236236

237-
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage, this.options)
237+
validateScalarTypeValue(context, currentField, typeDefWithDirective, valueTypeDef, element, variableName, argName, iFieldNameFullIndexed, atMessage, options)
238238
}
239239
})
240240
}
@@ -276,7 +276,7 @@ class InputObjectValidationVisitor {
276276

277277
validateInputTypeValue(this.context, valueTypeDef, this.argName, this.variableName, value, this.currentField, iFieldNameFull, this.options)
278278
} else if (isListType(valueTypeDef)) {
279-
validateArrayTypeValue(this.context, valueTypeDef, iFieldTypeDef, value, this.currentField, this.argName, this.variableName, iFieldNameFull)
279+
validateArrayTypeValue(this.context, valueTypeDef, iFieldTypeDef, value, this.currentField, this.argName, this.variableName, iFieldNameFull, this.options)
280280
} else {
281281
// nothing to validate
282282
if (!value && value !== '' && value !== 0) return

test/custom-format.test.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
const { strictEqual, deepStrictEqual } = require('assert')
2+
const { GraphQLError } = require('graphql')
3+
const { isStatusCodeError, IMPL_TYPE_SERVER_VALIDATOR_APOLLO4 } = require('./testutils')
4+
5+
module.exports.test = function (setup, implType) {
6+
describe('Custom format', () => {
7+
let typeDefs
8+
let request
9+
const fooFormat = (value) => {
10+
if (typeof value === 'string' && value.toLowerCase().includes('foo')) {
11+
return true
12+
}
13+
throw new GraphQLError('No foos', {
14+
extensions: {
15+
code: 'BAD_USER_INPUT',
16+
http: {
17+
status: 400
18+
}
19+
}
20+
})
21+
}
22+
const createBookMutation = `
23+
mutation CreateBook($input: BookInput!) {
24+
createBook(input: $input) {
25+
title
26+
authors
27+
}
28+
}
29+
`
30+
31+
describe('Array input', () => {
32+
before(async () => {
33+
typeDefs = `
34+
type Query {
35+
books: [Book!]
36+
}
37+
type Mutation {
38+
createBook(input: BookInput!): Book
39+
}
40+
type Book {
41+
title: String
42+
authors: [String!]
43+
}
44+
input BookInput {
45+
title: String!
46+
authors: [String!]! @constraint(format: "foo")
47+
}
48+
`
49+
request = await setup({ typeDefs, pluginOptions: { formats: { foo: fooFormat } } })
50+
})
51+
52+
it('should pass', async () => {
53+
const variables = {
54+
input: {
55+
title: '1984',
56+
authors: ['Foo Orwell', 'Another Foo']
57+
}
58+
}
59+
const { body, statusCode } = await request
60+
.post('/graphql')
61+
.set('Accept', 'application/json')
62+
.send({ query: createBookMutation, variables })
63+
64+
strictEqual(statusCode, 200)
65+
deepStrictEqual(body, { data: { createBook: null } })
66+
})
67+
68+
it('should fail', async () => {
69+
const variables = {
70+
input: {
71+
title: 'The Metamorphosis',
72+
authors: ['Franz Kafka', 'Stanley Corngold']
73+
}
74+
}
75+
const { body, statusCode } = await request
76+
.post('/graphql')
77+
.set('Accept', 'application/json')
78+
.send({ query: createBookMutation, variables })
79+
80+
isStatusCodeError(statusCode, implType)
81+
if (implType === IMPL_TYPE_SERVER_VALIDATOR_APOLLO4) {
82+
strictEqual(body.errors.length, 1)
83+
strictEqual(
84+
body.errors[0].extensions.validationErrors[0].message,
85+
'Variable "$input" got invalid value "Franz Kafka" at "input.authors[0]". No foos'
86+
)
87+
strictEqual(
88+
body.errors[0].extensions.validationErrors[1].message,
89+
'Variable "$input" got invalid value "Stanley Corngold" at "input.authors[1]". No foos'
90+
)
91+
} else {
92+
strictEqual(body.errors.length, 2)
93+
strictEqual(
94+
body.errors[0].message,
95+
'Variable "$input" got invalid value "Franz Kafka" at "input.authors[0]". No foos'
96+
)
97+
strictEqual(
98+
body.errors[1].message,
99+
'Variable "$input" got invalid value "Stanley Corngold" at "input.authors[1]". No foos'
100+
)
101+
}
102+
})
103+
})
104+
105+
describe('Scalar input', () => {
106+
before(async () => {
107+
typeDefs = `
108+
type Query {
109+
books: [Book!]
110+
}
111+
type Mutation {
112+
createBook(input: BookInput!): Book
113+
}
114+
type Book {
115+
title: String
116+
authors: [String!]
117+
}
118+
input BookInput {
119+
title: String! @constraint(format: "foo")
120+
authors: [String!]!
121+
}
122+
`
123+
request = await setup({ typeDefs, pluginOptions: { formats: { foo: fooFormat } } })
124+
})
125+
126+
it('should pass', async () => {
127+
const variables = {
128+
input: {
129+
title: 'Foos and Bars',
130+
authors: ['Tacitus Kilgore']
131+
}
132+
}
133+
const { body, statusCode } = await request
134+
.post('/graphql')
135+
.set('Accept', 'application/json')
136+
.send({ query: createBookMutation, variables })
137+
138+
strictEqual(statusCode, 200)
139+
deepStrictEqual(body, { data: { createBook: null } })
140+
})
141+
142+
it('should fail', async () => {
143+
const variables = {
144+
input: {
145+
title: '1984',
146+
authors: ['George Orwell']
147+
}
148+
}
149+
const { body, statusCode } = await request
150+
.post('/graphql')
151+
.set('Accept', 'application/json')
152+
.send({ query: createBookMutation, variables })
153+
154+
isStatusCodeError(statusCode, implType)
155+
strictEqual(body.errors.length, 1)
156+
strictEqual(
157+
body.errors[0].message,
158+
'Variable "$input" got invalid value "1984" at "input.title". No foos'
159+
)
160+
})
161+
})
162+
})
163+
}

test/testsuite-apollo-plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ describe('Server validator based implementation - Apollo plugin', function () {
2020
require('./union.test').test(setup, IMPL_TYPE)
2121
require('./foreign-query-directives.test').test(setup, IMPL_TYPE)
2222
require('./mutation-failure.test').test(setup, IMPL_TYPE)
23+
require('./custom-format.test').test(setup, IMPL_TYPE)
2324
})

test/testsuite-apollo4-plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ describe('Server validator based implementation - Apollo 4 plugin', function ()
1818
require('./string.test').test(setup, IMPL_TYPE)
1919
require('./argument-dynamic.test').test(setup, IMPL_TYPE)
2020
require('./union.test').test(setup, IMPL_TYPE)
21+
require('./custom-format.test').test(setup, IMPL_TYPE)
2122
})

test/testsuite-envelop-plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ describe('Server validator based implementation - Envelop plugin', function () {
1818
require('./string.test').test(setup, IMPL_TYPE)
1919
require('./argument-dynamic.test').test(setup, IMPL_TYPE)
2020
require('./union.test').test(setup, IMPL_TYPE)
21+
require('./custom-format.test').test(setup, IMPL_TYPE)
2122
})

0 commit comments

Comments
 (0)