Skip to content

Commit

Permalink
Merge branch 'mercurius-js:main' into fix/nullability
Browse files Browse the repository at this point in the history
  • Loading branch information
peterc1731 authored Oct 3, 2024
2 parents bf2760d + 2f9ed30 commit e948cd7
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 24 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [18.x, 20.x]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
Expand Down
1 change: 1 addition & 0 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Extends: [`AJVOptions`](https://ajv.js.org/options.html)
* **mode** `"JSONSchema" | "JTD"` (optional, default: `"JSONSchema"`) - the validation mode of the plugin. This is used to specify the type of schema that needs to be compiled.
* **schema** `MercuriusValidationSchema` (optional) - the validation schema definition that the plugin with run. One can define JSON Schema or JTD definitions for GraphQL types, fields and arguments or functions for GraphQL arguments.
* **directiveValidation** `boolean` (optional, default: `true`) - turn directive validation on or off. It is on by default.
* **customTypeInferenceFn** `Function` (optional) - add custom type inference for JSON Schema Types. This function overrides the default type inference logic which infers GraphQL primitives like `GraphQLString`, `GraphQLInt` and `GraphQLFloat`. If the custom function doesn't handle the passed type, then it should return a falsy value which will trigger the default type inference logic of the plugin. This function takes two parameters. The first parameter is `type` referring to the GraphQL type under inference, while the second one is `isNonNull`, a boolean value referring whether the value for the type is nullable.

It extends the [AJV options](https://ajv.js.org/options.html). These can be used to register additional `formats` for example and provide further customization to the AJV validation behavior.

Expand Down
22 changes: 22 additions & 0 deletions docs/json-schema-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,28 @@ app.register(mercuriusValidation, {
})
```

The type inference is customizable. You can pass `customTypeInferenceFn` in the plugin options and have your own inference logic inside the function. The below code is an example for custom type inference for `GraphQLBoolean` <=> `{ type: 'boolean' }`.

```js
app.register(mercuriusValidation, {
schema: {
Filters: {
isAvailable: { type: 'boolean' }
},
Query: {
product: {
id: { type: 'string', minLength: 1 }
}
}
},
customTypeInferenceFn: (type, isNonNull) => {
if (type === GraphQLBoolean) {
return isNonNull ? { type: 'boolean' } : { type: ['boolean', 'null'] }
}
}
})
```

## Caveats

The use of the `$ref` keyword is not advised because we use this through the plugin to build up the GraphQL type validation. However, we have not prevented use of this keyword since it may be useful in some situations.
8 changes: 7 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ function validateOpts (opts) {
return opts
}

function inferJSONSchemaType (type, isNonNull) {
function inferJSONSchemaType (type, isNonNull, customTypeInferenceFn) {
if (customTypeInferenceFn) {
const customResponse = customTypeInferenceFn(type, isNonNull)
if (customResponse) {
return customResponse
}
}
if (type === GraphQLString) {
return isNonNull ? { type: 'string' } : { type: ['string', 'null'] }
}
Expand Down
4 changes: 2 additions & 2 deletions lib/validators/json-schema-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { getTypeInfo, inferJSONSchemaType } = require('../utils')
class JSONSchemaValidator extends Validator {
[kValidationSchema] (type, namedType, isNonNull, typeValidation, id) {
let builtValidationSchema = {
...inferJSONSchemaType(namedType, isNonNull),
...inferJSONSchemaType(namedType, isNonNull, this[kOpts].customTypeInferenceFn),
$id: id
}

Expand All @@ -31,7 +31,7 @@ class JSONSchemaValidator extends Validator {
}
// If we have an array of scalars, set the array type and infer the items
} else if (isListType(type)) {
let items = { ...inferJSONSchemaType(namedType, isNonNull), ...builtValidationSchema.items }
let items = { ...inferJSONSchemaType(namedType, isNonNull, this[kOpts].customTypeInferenceFn), ...builtValidationSchema.items }
if (typeValidation !== null) {
items = { ...items, ...typeValidation.items }
}
Expand Down
34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mercurius-validation",
"version": "4.0.0",
"version": "5.0.0",
"description": "Mercurius Validation Plugin adds configurable Validation support to Mercurius.",
"main": "index.js",
"types": "index.d.ts",
Expand Down Expand Up @@ -34,31 +34,31 @@
},
"homepage": "https://github.com/mercurius-js/validation",
"devDependencies": {
"@mercuriusjs/federation": "^2.0.0",
"@mercuriusjs/gateway": "^1.0.0",
"@sinonjs/fake-timers": "^10.0.2",
"@types/node": "^20.1.0",
"@types/ws": "^8.5.3",
"@mercuriusjs/federation": "^3.0.0",
"@mercuriusjs/gateway": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@types/node": "^22.0.0",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.30.3",
"@typescript-eslint/parser": "^5.30.3",
"autocannon": "^7.9.0",
"concurrently": "^8.0.1",
"fastify": "^4.2.0",
"mercurius": "^13.0.0",
"autocannon": "^7.15.0",
"concurrently": "^9.0.0",
"fastify": "^4.26.2",
"mercurius": "^14.0.0",
"pre-commit": "^1.2.2",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"standard": "^17.1.0",
"tap": "^16.3.0",
"tsd": "^0.28.0",
"typescript": "^5.0.2",
"wait-on": "^7.0.1"
"tsd": "^0.31.0",
"typescript": "^5.4.2",
"wait-on": "^8.0.0"
},
"dependencies": {
"@fastify/error": "^3.0.0",
"@fastify/error": "^4.0.0",
"ajv": "^8.6.2",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"fastify-plugin": "^4.0.0",
"ajv-formats": "^3.0.1",
"fastify-plugin": "^5.0.1",
"graphql": "^16.2.0"
},
"tsd": {
Expand Down
123 changes: 122 additions & 1 deletion test/json-schema-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Fastify = require('fastify')
const mercurius = require('mercurius')
const mercuriusValidation = require('..')
const { MER_VALIDATION_ERR_FIELD_TYPE_UNDEFINED } = require('../lib/errors')
const { GraphQLBoolean } = require('graphql')

const schema = `
type Message {
Expand Down Expand Up @@ -69,7 +70,7 @@ const resolvers = {
}

t.test('JSON Schema validators', t => {
t.plan(18)
t.plan(19)

t.test('should protect the schema and not affect operations when everything is okay', async (t) => {
t.plan(1)
Expand Down Expand Up @@ -2126,4 +2127,124 @@ t.test('JSON Schema validators', t => {
}
})
})

t.test('should invoke customTypeInferenceFn option and not affect operations when everything is okay', async (t) => {
const productSchema = `
type Product {
id: ID!
text: String
isAvailable: Boolean
}
input Filters {
id: ID
text: String
isAvailable: Boolean
}
type Query {
noResolver(id: ID): ID
product(id: ID): Product
products(
filters: Filters
): [Product]
}
`

const products = [
{
id: 0,
text: 'Phone',
isAvailable: true
},
{
id: 1,
text: 'Laptop',
isAvailable: true
},
{
id: 2,
text: 'Keyboard',
isAvailable: false
}
]

const productResolvers = {
Query: {
product: async (_, { id }) => {
return products.find(product => product.id === Number(id))
},
products: async (_, { filters }) => {
return products.filter(product => product.isAvailable === filters.isAvailable)
}
}
}

const app = Fastify()
t.teardown(app.close.bind(app))

app.register(mercurius, {
schema: productSchema,
resolvers: productResolvers
})
app.register(mercuriusValidation, {
schema: {
Filters: {
isAvailable: { type: 'boolean' }
},
Query: {
product: {
id: { type: 'string', minLength: 1 }
}
}
},
customTypeInferenceFn: (type, isNonNull) => {
if (type === GraphQLBoolean) {
return isNonNull ? { type: 'boolean' } : { type: ['boolean', 'null'] }
}
}
})

const query = `query {
product(id: "1") {
id
text
isAvailable
}
products(filters: { isAvailable: true }) {
id
text
isAvailable
}
}`

const response = await app.inject({
method: 'POST',
headers: { 'content-type': 'application/json' },
url: '/graphql',
body: JSON.stringify({ query })
})

t.same(JSON.parse(response.body), {
data: {
product: {
id: 1,
text: 'Laptop',
isAvailable: true
},
products: [
{
id: 0,
text: 'Phone',
isAvailable: true
},
{
id: 1,
text: 'Laptop',
isAvailable: true
}
]
}
})
})
})

0 comments on commit e948cd7

Please sign in to comment.