Skip to content

[WIP] Add support of nested logical combinators (fix #8) #11

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Query {
emailAddr: String @constraint(format: "email")
otherEmailAddr: String @constraint(format: "email", differsFrom: "emailAddr")
age: Int @constraint(min: 18)
bio: String @constraint(OR: [{contains: "foo"}, {contains: "bar"}])
): User
}
```
Expand Down Expand Up @@ -54,6 +55,9 @@ You may need to declare the directive in the schema:

```graphql
directive @constraint(
OR: [ConstraintInput!],
NOT: [ConstraintInput!],
AND: [ConstraintInput!],
minLength: Int
maxLength: Int
startsWith: String
Expand All @@ -69,6 +73,26 @@ directive @constraint(
exclusiveMax: Float
notEqual: Float
) on ARGUMENT_DEFINITION

input ConstraintInput {
OR: [ConstraintInput!]
NOT: [ConstraintInput!]
AND: [ConstraintInput!]
minLength: Int
maxLength: Int
startsWith: String
endsWith: String
contains: String
notContains: String
pattern: String
format: String
differsFrom: String
min: Float
max: Float
exclusiveMin: Float
exclusiveMax: Float
notEqual: Float
}
```

## API
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"git-cred": "git config credential.helper store",
"lint": "eslint .",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest",
"release": "standard-version",
"release:push": "git push --follow-tags origin master",
"release:npm": "yarn publish"
Expand Down
76 changes: 55 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
// @ts-check
/**
* Support for code assist and type checing in vscode
* @typedef {import("graphql").GraphQLInterfaceType} GraphQLInterfaceType
* @typedef {import("graphql").GraphQLObjectType} GraphQLObjectType
* @typedef {import("graphql").GraphQLField} GraphQLField
* @typedef {import("graphql").GraphQLArgument} GraphQLArgument
*/

const { mapObjIndexed, compose, map, filter, values } = require('./utils')
const { SchemaDirectiveVisitor } = require('graphql-tools')
const {
Expand All @@ -12,52 +21,76 @@ const {
GraphQLFloat,
GraphQLString,
GraphQLSchema,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
printSchema
} = require('graphql')

const prepareConstraintDirective = (validationCallback, errorMessageCallback) =>
class extends SchemaDirectiveVisitor {
class ConstraintDirectiveVisitor extends SchemaDirectiveVisitor {
/**
* When using e.g. graphql-yoga, we need to include schema of this directive
* into our SDL, otherwise the graphql schema validator would report errors.
*/
static getSDL () {
const constraintDirective = this.getDirectiveDeclaration('constraint')
const thisDirective = this.getDirectiveDeclaration('constraint', null)
const schema = new GraphQLSchema({
directives: [constraintDirective]
query: undefined,
directives: [thisDirective]
})
return printSchema(schema)
}

/**
* @param {string} directiveName
* @param {GraphQLSchema} schema
*/
static getDirectiveDeclaration (directiveName, schema) {
const constraintInput = new GraphQLNonNull(new GraphQLInputObjectType({
name: 'ConstraintInput',
fields: () => ({
...args
})
}))

const args = {
/* Logical combinators */
OR: { type: new GraphQLList(constraintInput) },
NOT: { type: new GraphQLList(constraintInput) },
AND: { type: new GraphQLList(constraintInput) },

/* Strings */
minLength: { type: GraphQLInt },
maxLength: { type: GraphQLInt },
startsWith: { type: GraphQLString },
endsWith: { type: GraphQLString },
contains: { type: GraphQLString },
notContains: { type: GraphQLString },
pattern: { type: GraphQLString },
format: { type: GraphQLString },
differsFrom: { type: GraphQLString },

/* Numbers (Int/Float) */
min: { type: GraphQLFloat },
max: { type: GraphQLFloat },
exclusiveMin: { type: GraphQLFloat },
exclusiveMax: { type: GraphQLFloat },
notEqual: { type: GraphQLFloat }
}

return new GraphQLDirective({
name: directiveName,
locations: [DirectiveLocation.ARGUMENT_DEFINITION],
args: {
/* Strings */
minLength: { type: GraphQLInt },
maxLength: { type: GraphQLInt },
startsWith: { type: GraphQLString },
endsWith: { type: GraphQLString },
contains: { type: GraphQLString },
notContains: { type: GraphQLString },
pattern: { type: GraphQLString },
format: { type: GraphQLString },
differsFrom: { type: GraphQLString },

/* Numbers (Int/Float) */
min: { type: GraphQLFloat },
max: { type: GraphQLFloat },
exclusiveMin: { type: GraphQLFloat },
exclusiveMax: { type: GraphQLFloat },
notEqual: { type: GraphQLFloat }
...args
}
})
}

/**
* @param {GraphQLArgument} argument
* @param {{field:GraphQLField<any, any>, objectType:GraphQLObjectType | GraphQLInterfaceType}} details
* @param {{field:GraphQLField, objectType:GraphQLObjectType | GraphQLInterfaceType}} details
*/
visitArgumentDefinition (argument, details) {
// preparing the resolver
Expand All @@ -76,6 +109,7 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) =>
)
)

// validation starts here and errors are collected
const errors = validate(this.args)
if (errors && errors.length > 0) throw new Error(errors)

Expand Down
14 changes: 13 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
// api same as ramda
// @ts-check

/** api same as ramda */
const map = fn => list => list.map(fn)

/** api same as ramda */
const filter = fn => list => list.filter(fn)

/** api same as ramda */
const values = obj => Object.keys(obj).map(key => obj[key])

/** api same as ramda */
const length = strOrArray => (strOrArray != null ? strOrArray.length : 0)

/** api same as ramda */
const isString = x => x != null && x.constructor === String

/** api same as ramda */
const compose = (...fnlist) => data =>
[...fnlist, data].reduceRight((prev, fn) => fn(prev))

/** api same as ramda */
const mapObjIndexed = fn => obj => {
const acc = {}
Object.keys(obj).forEach(key => (acc[key] = fn(obj[key], key, obj)))
Expand Down
9 changes: 9 additions & 0 deletions src/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ const numericValidators = {
notEqual: neq => x => x !== neq
}

// TODO: implement it
const logicalValidators = {
// OR: ,
// AND: ,
// NOT:
}

const defaultErrorMessageCallback = ({ argName, cName, cVal, data }) =>
`Constraint '${cName}:${cVal}' violated in field '${argName}'`

const defaultValidators = {
...formatValidator(format2fun),
...numericValidators,
...logicalValidators,
...stringValidators
}

Expand All @@ -73,6 +81,7 @@ module.exports = {
createValidationCallback,
stringValidators,
numericValidators,
logicalValidators,
formatValidator,
format2fun
}
46 changes: 46 additions & 0 deletions test/class.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-check

const { makeExecutableSchema } = require('graphql-tools')
const { GraphQLSchema } = require('graphql')
const { constraint } = require('../src/index')

describe('constraint directive class', () => {
it('should provide its own graphql SDL', () => {
const sdl = constraint.getSDL()
expect(sdl).toMatch('directive @constraint')
})

it('should work when used properly in other graphql schema', () => {
const withOtherSchema = `
${constraint.getSDL()}
type Mutation {
signup(
name: String @constraint(maxLength:20)
): Boolean
}
`
const schema = makeExecutableSchema({
typeDefs: withOtherSchema,
schemaDirectives: { constraint }
})

expect(schema).toBeInstanceOf(GraphQLSchema)
})

it('should NOT work when using unknown parameter', () => {
const withOtherSchema = `
${constraint.getSDL()}
type Mutation {
signup(
name: String @constraint(DUMMY:123)
): Boolean
}
`
expect(() =>
makeExecutableSchema({
typeDefs: withOtherSchema,
schemaDirectives: { constraint }
})
).toThrowError('Unknown argument "DUMMY" on directive "@constraint"')
})
})
Loading