From 0652215144259473edd7bbc1eaec118672c8d410 Mon Sep 17 00:00:00 2001 From: Alexey Rodionov Date: Fri, 8 Feb 2019 10:11:15 +0300 Subject: [PATCH 01/10] Prepare adding support of nested logical combinators --- README.md | 18 ++++++++++++------ src/index.js | 21 +++++++++++++++++---- src/utils.js | 2 ++ src/validators.js | 6 +++++- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b862e4f..8f85572 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ Example GraphQL Schema: ```graphql type Query { createUser ( - name: String! @constraint(minLength: 5, maxLength: 40) - emailAddr: String @constraint(format: "email") - otherEmailAddr: String @constraint(format: "email", differsFrom: "emailAddr") - age: Int @constraint(min: 18) + name: String! @constraint(where: {minLength: 5, maxLength: 40}) + emailAddr: String @constraint(where: {format: "email"}) + otherEmailAddr: String @constraint(where: {format: "email", differsFrom: "emailAddr"}) + age: Int @constraint(where: {min: 18}) + bio: String @constraint(where: {OR: [{contains: "foo"}, {contains: "bar"}]}) ): User } ``` @@ -53,7 +54,12 @@ const server = new GraphQLServer({ You may need to declare the directive in the schema: ```graphql -directive @constraint( +directive @constraint(where: constraintsWhereInput!) on ARGUMENT_DEFINITION + +input constraintsWhereInput { + AND: [constraintsWhereInput!] + OR: [constraintsWhereInput!] + NOT: [constraintsWhereInput!] minLength: Int maxLength: Int startsWith: String @@ -68,7 +74,7 @@ directive @constraint( exclusiveMin: Float exclusiveMax: Float notEqual: Float -) on ARGUMENT_DEFINITION +} ``` ## API diff --git a/src/index.js b/src/index.js index 8ae55d7..c5925e6 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,9 @@ const { GraphQLFloat, GraphQLString, GraphQLSchema, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, printSchema } = require('graphql') @@ -30,11 +33,13 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => } static getDirectiveDeclaration (directiveName, schema) { - return new GraphQLDirective({ - name: directiveName, - locations: [DirectiveLocation.ARGUMENT_DEFINITION], - args: { + const constraintsWhereInput = new GraphQLNonNull(new GraphQLInputObjectType({ + name: 'constraintsWhereInput', + fields: () => ({ /* Strings */ + AND: { type: new GraphQLList(constraintsWhereInput) }, + OR: { type: new GraphQLList(constraintsWhereInput) }, + NOT: { type: new GraphQLList(constraintsWhereInput) }, minLength: { type: GraphQLInt }, maxLength: { type: GraphQLInt }, startsWith: { type: GraphQLString }, @@ -51,6 +56,14 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => exclusiveMin: { type: GraphQLFloat }, exclusiveMax: { type: GraphQLFloat }, notEqual: { type: GraphQLFloat } + }) + })); + + return new GraphQLDirective({ + name: directiveName, + locations: [DirectiveLocation.ARGUMENT_DEFINITION], + args: { + where: { type: constraintsWhereInput } } }) } diff --git a/src/utils.js b/src/utils.js index 9085978..380dc6d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,6 +9,8 @@ const compose = (...fnlist) => data => [...fnlist, data].reduceRight((prev, fn) => fn(prev)) const mapObjIndexed = fn => obj => { + //FIXME: sure, there is a better way to do this + obj = obj['where']; const acc = {} Object.keys(obj).forEach(key => (acc[key] = fn(obj[key], key, obj))) return acc diff --git a/src/validators.js b/src/validators.js index 2f97e62..426aeec 100644 --- a/src/validators.js +++ b/src/validators.js @@ -49,7 +49,11 @@ const numericValidators = { max: max => x => x <= max, exclusiveMin: min => x => x > min, exclusiveMax: max => x => x < max, - notEqual: neq => x => x !== neq + notEqual: neq => x => x !== neq, + //TODO: implement the following validators + //OR: , + //AND: , + //NOT: } const defaultErrorMessageCallback = ({ argName, cName, cVal, data }) => From d0f5eb95d69a311441f06c945618c62b467fd3ee Mon Sep 17 00:00:00 2001 From: Alexey Rodionov Date: Fri, 8 Feb 2019 10:18:10 +0300 Subject: [PATCH 02/10] Move OR, AND, NOT to logicalValidators --- src/validators.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/validators.js b/src/validators.js index 426aeec..601e40f 100644 --- a/src/validators.js +++ b/src/validators.js @@ -49,11 +49,14 @@ const numericValidators = { max: max => x => x <= max, exclusiveMin: min => x => x > min, exclusiveMax: max => x => x < max, - notEqual: neq => x => x !== neq, - //TODO: implement the following validators + notEqual: neq => x => x !== neq +} + +//TODO: implement it +const logicalValidators = { //OR: , //AND: , - //NOT: + //NOT: } const defaultErrorMessageCallback = ({ argName, cName, cVal, data }) => @@ -62,6 +65,7 @@ const defaultErrorMessageCallback = ({ argName, cName, cVal, data }) => const defaultValidators = { ...formatValidator(format2fun), ...numericValidators, + ...logicalValidators, ...stringValidators } @@ -77,6 +81,7 @@ module.exports = { createValidationCallback, stringValidators, numericValidators, + logicalValidators, formatValidator, format2fun } From 6c432c985f31d642d514671daeb9b4b675d31b12 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 09:31:15 +0100 Subject: [PATCH 03/10] chore: happy lint --- src/index.js | 50 ++++++++++++++++++++++++----------------------- src/utils.js | 4 ++-- src/validators.js | 8 ++++---- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/index.js b/src/index.js index c5925e6..cf74857 100644 --- a/src/index.js +++ b/src/index.js @@ -33,37 +33,39 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => } static getDirectiveDeclaration (directiveName, schema) { - const constraintsWhereInput = new GraphQLNonNull(new GraphQLInputObjectType({ - name: 'constraintsWhereInput', - fields: () => ({ - /* Strings */ - AND: { type: new GraphQLList(constraintsWhereInput) }, - OR: { type: new GraphQLList(constraintsWhereInput) }, - NOT: { type: new GraphQLList(constraintsWhereInput) }, - 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 }, + const constraintsWhereInput = new GraphQLNonNull( + new GraphQLInputObjectType({ + name: 'constraintsWhereInput', + fields: () => ({ + /* Strings */ + AND: { type: new GraphQLList(constraintsWhereInput) }, + OR: { type: new GraphQLList(constraintsWhereInput) }, + NOT: { type: new GraphQLList(constraintsWhereInput) }, + 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 } + /* 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: { - where: { type: constraintsWhereInput } + where: { type: constraintsWhereInput } } }) } diff --git a/src/utils.js b/src/utils.js index 380dc6d..b522be5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,8 +9,8 @@ const compose = (...fnlist) => data => [...fnlist, data].reduceRight((prev, fn) => fn(prev)) const mapObjIndexed = fn => obj => { - //FIXME: sure, there is a better way to do this - obj = obj['where']; + // FIXME: sure, there is a better way to do this + obj = obj['where'] const acc = {} Object.keys(obj).forEach(key => (acc[key] = fn(obj[key], key, obj))) return acc diff --git a/src/validators.js b/src/validators.js index 601e40f..dba4ba7 100644 --- a/src/validators.js +++ b/src/validators.js @@ -52,11 +52,11 @@ const numericValidators = { notEqual: neq => x => x !== neq } -//TODO: implement it +// TODO: implement it const logicalValidators = { - //OR: , - //AND: , - //NOT: + // OR: , + // AND: , + // NOT: } const defaultErrorMessageCallback = ({ argName, cName, cVal, data }) => From 318b12b9d064240849c3db799c2bb37bf28af640 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 10:32:42 +0100 Subject: [PATCH 04/10] test: splitting test file --- test/class.test.js | 44 ++++++++++++++ test/test.js | 146 --------------------------------------------- test/usage.test.js | 102 +++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 test/class.test.js delete mode 100644 test/test.js create mode 100644 test/usage.test.js diff --git a/test/class.test.js b/test/class.test.js new file mode 100644 index 0000000..2da0e40 --- /dev/null +++ b/test/class.test.js @@ -0,0 +1,44 @@ +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"') + }) +}) diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 565ff27..0000000 --- a/test/test.js +++ /dev/null @@ -1,146 +0,0 @@ -const { makeExecutableSchema } = require('graphql-tools') -const { GraphQLSchema } = require('graphql') - -const { constraint, prepareConstraintDirective } = require('../src/index') -const { - format2fun, - defaultValidators, - defaultValidationCallback, - defaultErrorMessageCallback -} = require('../src/validators') - -describe('constraint directive usage', () => { - it('customization of messages', async () => { - // this is just an example showing that it works, it can be implemented better - const customizedMessageCallback = input => { - if (input.argName === 'primaryEmail') return 'Wrong primary email' - else return defaultErrorMessageCallback(input) - } - - const MyConstraintClass = prepareConstraintDirective( - defaultValidationCallback, - customizedMessageCallback - ) - - const cInst = new MyConstraintClass({ args: { format: 'email' } }) - - const details = { field: {} } - - cInst.visitArgumentDefinition({ name: 'primaryEmail' }, details) - await expect( - details.field.resolve(null, { primaryEmail: 'some_wrong_email' }) - ).rejects.toEqual(Error([`Wrong primary email`])) - - cInst.visitArgumentDefinition({ name: 'secondaryEmail' }, details) - await expect( - details.field.resolve(null, { secondaryEmail: 'some_wrong_email' }) - ).rejects.toEqual( - Error([`Constraint 'format:email' violated in field 'secondaryEmail'`]) - ) - }) - - it('constraint violations should be reported', async () => { - // eslint-disable-next-line new-cap - const cInst = new constraint({ - args: { - maxLength: 5, - format: 'email', - minLength: 100 - } - }) - - const details = { field: {} } - cInst.visitArgumentDefinition({ name: 'primaryEmail' }, details) - - await expect( - details.field.resolve(null, { primaryEmail: 'test@test.com' }) - ).rejects.toEqual( - Error([ - `Constraint 'maxLength:5' violated in field 'primaryEmail'`, - `Constraint 'minLength:100' violated in field 'primaryEmail'` - ]) - ) - }) - - describe('format2fun', () => { - it('email', () => { - expect(format2fun.email('test@test.com')).toEqual(true) - expect(format2fun.email('testtest.com')).toEqual(false) - }) - }) - - describe('defaultValidators', () => { - it('contains', () => { - const { contains } = defaultValidators - expect(contains('@')('test@test.com')).toEqual(true) - expect(contains('@')('testtest.com')).toEqual(false) - }) - it('startsWith', () => { - const { startsWith } = defaultValidators - expect(startsWith('b')('ab')).toEqual(false) - expect(startsWith('a')('ab')).toEqual(true) - expect(startsWith('')('')).toEqual(true) - expect(startsWith('')('a')).toEqual(true) - expect(startsWith('a')('')).toEqual(false) - }) - - it('endsWith', () => { - const { endsWith } = defaultValidators - expect(endsWith('b')('ab')).toEqual(true) - expect(endsWith('a')('ab')).toEqual(false) - expect(endsWith('')('')).toEqual(true) - expect(endsWith('')('a')).toEqual(true) - expect(endsWith('a')('')).toEqual(false) - }) - - it('minLength', () => { - const { minLength } = defaultValidators - expect(minLength(10)('ab')).toEqual(false) - expect(minLength(2)('ab')).toEqual(true) - expect(minLength(0)('ab')).toEqual(true) - expect(minLength(0)('')).toEqual(true) - expect(minLength(1)('')).toEqual(false) - }) - }) -}) - -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"') - }) -}) diff --git a/test/usage.test.js b/test/usage.test.js new file mode 100644 index 0000000..4ff9a49 --- /dev/null +++ b/test/usage.test.js @@ -0,0 +1,102 @@ +const { constraint, prepareConstraintDirective } = require('../src/index') +const { + format2fun, + defaultValidators, + defaultValidationCallback, + defaultErrorMessageCallback +} = require('../src/validators') + +describe('constraint directive usage', () => { + it('customization of messages', async () => { + // this is just an example showing that it works, it can be implemented better + const customizedMessageCallback = input => { + if (input.argName === 'primaryEmail') return 'Wrong primary email' + else return defaultErrorMessageCallback(input) + } + + const MyConstraintClass = prepareConstraintDirective( + defaultValidationCallback, + customizedMessageCallback + ) + + const cInst = new MyConstraintClass({ args: { format: 'email' } }) + + const details = { field: {} } + + cInst.visitArgumentDefinition({ name: 'primaryEmail' }, details) + await expect( + details.field.resolve(null, { primaryEmail: 'some_wrong_email' }) + ).rejects.toEqual(Error([`Wrong primary email`])) + + cInst.visitArgumentDefinition({ name: 'secondaryEmail' }, details) + await expect( + details.field.resolve(null, { secondaryEmail: 'some_wrong_email' }) + ).rejects.toEqual( + Error([`Constraint 'format:email' violated in field 'secondaryEmail'`]) + ) + }) + + it('constraint violations should be reported', async () => { + // eslint-disable-next-line new-cap + const cInst = new constraint({ + args: { + maxLength: 5, + format: 'email', + minLength: 100 + } + }) + + const details = { field: {} } + cInst.visitArgumentDefinition({ name: 'primaryEmail' }, details) + + await expect( + details.field.resolve(null, { primaryEmail: 'test@test.com' }) + ).rejects.toEqual( + Error([ + `Constraint 'maxLength:5' violated in field 'primaryEmail'`, + `Constraint 'minLength:100' violated in field 'primaryEmail'` + ]) + ) + }) +}) + +describe('format2fun', () => { + it('email', () => { + expect(format2fun.email('test@test.com')).toEqual(true) + expect(format2fun.email('testtest.com')).toEqual(false) + }) +}) + +describe('defaultValidators', () => { + it('contains', () => { + const { contains } = defaultValidators + expect(contains('@')('test@test.com')).toEqual(true) + expect(contains('@')('testtest.com')).toEqual(false) + }) + it('startsWith', () => { + const { startsWith } = defaultValidators + expect(startsWith('b')('ab')).toEqual(false) + expect(startsWith('a')('ab')).toEqual(true) + expect(startsWith('')('')).toEqual(true) + expect(startsWith('')('a')).toEqual(true) + expect(startsWith('a')('')).toEqual(false) + }) + + it('endsWith', () => { + const { endsWith } = defaultValidators + expect(endsWith('b')('ab')).toEqual(true) + expect(endsWith('a')('ab')).toEqual(false) + expect(endsWith('')('')).toEqual(true) + expect(endsWith('')('a')).toEqual(true) + expect(endsWith('a')('')).toEqual(false) + }) + + it('minLength', () => { + const { minLength } = defaultValidators + expect(minLength(10)('ab')).toEqual(false) + expect(minLength(2)('ab')).toEqual(true) + expect(minLength(0)('ab')).toEqual(true) + expect(minLength(0)('')).toEqual(true) + expect(minLength(1)('')).toEqual(false) + }) +}) From 08efa61e68dbe38486a4a038df1d7581c1be2a46 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 10:37:33 +0100 Subject: [PATCH 05/10] add doc-comments to functions inspired by ramda --- src/utils.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index b522be5..6d6f224 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,16 +1,26 @@ -// 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 => { - // FIXME: sure, there is a better way to do this - obj = obj['where'] const acc = {} Object.keys(obj).forEach(key => (acc[key] = fn(obj[key], key, obj))) return acc From 2b1728b024cd651ccba2ab879b3315d75110aae5 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 10:38:35 +0100 Subject: [PATCH 06/10] make args of the directive backwards compatible (and flatter) --- src/index.js | 76 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/index.js b/src/index.js index cf74857..8384154 100644 --- a/src/index.js +++ b/src/index.js @@ -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 { @@ -14,57 +23,64 @@ const { 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 constraintsWhereInput = new GraphQLNonNull( - new GraphQLInputObjectType({ - name: 'constraintsWhereInput', - fields: () => ({ - /* Strings */ - AND: { type: new GraphQLList(constraintsWhereInput) }, - OR: { type: new GraphQLList(constraintsWhereInput) }, - NOT: { type: new GraphQLList(constraintsWhereInput) }, - 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 }, + const simpleArgs = { + /* 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 } - }) + /* Numbers (Int/Float) */ + min: { type: GraphQLFloat }, + max: { type: GraphQLFloat }, + exclusiveMin: { type: GraphQLFloat }, + exclusiveMax: { type: GraphQLFloat }, + notEqual: { type: GraphQLFloat } + } + + const constraintsWhereInput = new GraphQLInputObjectType({ + name: 'constraintsWhereInput', + fields: () => ({ + ...simpleArgs, + AND: { type: new GraphQLList(constraintsWhereInput) }, + OR: { type: new GraphQLList(constraintsWhereInput) }, + NOT: { type: new GraphQLList(constraintsWhereInput) } }) - ) + }) return new GraphQLDirective({ name: directiveName, locations: [DirectiveLocation.ARGUMENT_DEFINITION], args: { + ...simpleArgs, where: { type: constraintsWhereInput } } }) @@ -72,7 +88,7 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => /** * @param {GraphQLArgument} argument - * @param {{field:GraphQLField, objectType:GraphQLObjectType | GraphQLInterfaceType}} details + * @param {{field:GraphQLField, objectType:GraphQLObjectType | GraphQLInterfaceType}} details */ visitArgumentDefinition (argument, details) { // preparing the resolver From eed312f1996fca3efc14b0ee0aaa74750bf57067 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 10:39:13 +0100 Subject: [PATCH 07/10] easier debugging of tests in vscode, just run `yarn test:debug` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3d03d7b..ffd30af 100644 --- a/package.json +++ b/package.json @@ -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" From c4a9f09ccf7a643f9de7bb14d75be4090f01fe11 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 10:39:26 +0100 Subject: [PATCH 08/10] enable ts-check in class.test.js --- test/class.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/class.test.js b/test/class.test.js index 2da0e40..2e7a7e2 100644 --- a/test/class.test.js +++ b/test/class.test.js @@ -1,3 +1,5 @@ +// @ts-check + const { makeExecutableSchema } = require('graphql-tools') const { GraphQLSchema } = require('graphql') const { constraint } = require('../src/index') From 8e43e9e1d08b2677f0fbbb5648c7022935531944 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Fri, 8 Feb 2019 11:33:21 +0100 Subject: [PATCH 09/10] test: constraints nested under the "where" clause (yet to be implemented) --- src/index.js | 1 + test/usage.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/index.js b/src/index.js index 8384154..76817b0 100644 --- a/src/index.js +++ b/src/index.js @@ -107,6 +107,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) diff --git a/test/usage.test.js b/test/usage.test.js index 4ff9a49..31c3a54 100644 --- a/test/usage.test.js +++ b/test/usage.test.js @@ -60,6 +60,31 @@ describe('constraint directive usage', () => { }) }) +describe('nested logical operators', () => { + it('TODO', async () => { + // eslint-disable-next-line new-cap + const cInst = new constraint({ + args: { + where: { + maxLength: 5, + minLength: 100 + } + } + }) + + const details = { field: {} } + cInst.visitArgumentDefinition({ name: 'primaryEmail' }, details) + await expect( + details.field.resolve(null, { primaryEmail: 'test@test.com' }) + ).rejects.toEqual( + Error([ + `Constraint 'maxLength:5' violated in field 'primaryEmail'`, + `Constraint 'minLength:100' violated in field 'primaryEmail'` + ]) + ) + }) +}) + describe('format2fun', () => { it('email', () => { expect(format2fun.email('test@test.com')).toEqual(true) From 3b377b2d0f277be1a59f595a30a151172c5c36eb Mon Sep 17 00:00:00 2001 From: Alexey Rodionov Date: Fri, 8 Feb 2019 20:24:03 +0300 Subject: [PATCH 10/10] Avoid the 'where' clause --- README.md | 38 ++++++++++++++++++++++++++++---------- src/index.js | 28 +++++++++++++++------------- test/usage.test.js | 8 ++++---- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 8f85572..df00921 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ Example GraphQL Schema: ```graphql type Query { createUser ( - name: String! @constraint(where: {minLength: 5, maxLength: 40}) - emailAddr: String @constraint(where: {format: "email"}) - otherEmailAddr: String @constraint(where: {format: "email", differsFrom: "emailAddr"}) - age: Int @constraint(where: {min: 18}) - bio: String @constraint(where: {OR: [{contains: "foo"}, {contains: "bar"}]}) + name: String! @constraint(minLength: 5, maxLength: 40) + 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 } ``` @@ -54,12 +54,30 @@ const server = new GraphQLServer({ You may need to declare the directive in the schema: ```graphql -directive @constraint(where: constraintsWhereInput!) on ARGUMENT_DEFINITION +directive @constraint( + 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 +) on ARGUMENT_DEFINITION -input constraintsWhereInput { - AND: [constraintsWhereInput!] - OR: [constraintsWhereInput!] - NOT: [constraintsWhereInput!] +input ConstraintInput { + OR: [ConstraintInput!] + NOT: [ConstraintInput!] + AND: [ConstraintInput!] minLength: Int maxLength: Int startsWith: String diff --git a/src/index.js b/src/index.js index 76817b0..193348f 100644 --- a/src/index.js +++ b/src/index.js @@ -23,6 +23,7 @@ const { GraphQLSchema, GraphQLInputObjectType, GraphQLList, + GraphQLNonNull, printSchema } = require('graphql') @@ -46,7 +47,19 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => * @param {GraphQLSchema} schema */ static getDirectiveDeclaration (directiveName, schema) { - const simpleArgs = { + 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 }, @@ -66,22 +79,11 @@ const prepareConstraintDirective = (validationCallback, errorMessageCallback) => notEqual: { type: GraphQLFloat } } - const constraintsWhereInput = new GraphQLInputObjectType({ - name: 'constraintsWhereInput', - fields: () => ({ - ...simpleArgs, - AND: { type: new GraphQLList(constraintsWhereInput) }, - OR: { type: new GraphQLList(constraintsWhereInput) }, - NOT: { type: new GraphQLList(constraintsWhereInput) } - }) - }) - return new GraphQLDirective({ name: directiveName, locations: [DirectiveLocation.ARGUMENT_DEFINITION], args: { - ...simpleArgs, - where: { type: constraintsWhereInput } + ...args } }) } diff --git a/test/usage.test.js b/test/usage.test.js index 31c3a54..f93d8b3 100644 --- a/test/usage.test.js +++ b/test/usage.test.js @@ -65,10 +65,10 @@ describe('nested logical operators', () => { // eslint-disable-next-line new-cap const cInst = new constraint({ args: { - where: { - maxLength: 5, - minLength: 100 - } + OR: [ + { contains: "foo" }, + { contains: "bar" } + ] } })