Skip to content

Commit c82781d

Browse files
authored
feat: added uniqueTypeName as an optional directiveArgument (#43)
1 parent 30a8f43 commit c82781d

File tree

5 files changed

+251
-7
lines changed

5 files changed

+251
-7
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,6 @@ const formatError = function (error) {
129129
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, formatError }))
130130

131131
```
132+
### uniqueTypeName
133+
```@constraint(uniqueTypeName: "Unique_Type_Name")```
134+
Override the unique type name generate by the library to the one passed as an argument

index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ function constraintDirective () {
88

99
function getConstraintType (fieldName, type, notNull, directiveArgumentMap) {
1010
// Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ as per graphql-js
11-
const uniqueTypeName = `${fieldName}_${type.name}_${notNull ? 'NotNull_' : ''}` + Object.entries(directiveArgumentMap)
12-
.map(([key, value]) => `${key}_${value.toString().replace(/\W/g, '')}`)
13-
.join('_')
11+
let uniqueTypeName
12+
if (directiveArgumentMap.uniqueTypeName) {
13+
uniqueTypeName = directiveArgumentMap.uniqueTypeName.replace(/\W/g, '')
14+
} else {
15+
uniqueTypeName = `${fieldName}_${type.name}_${notNull ? 'NotNull_' : ''}` + Object.entries(directiveArgumentMap)
16+
.map(([key, value]) => `${key}_${value.toString().replace(/\W/g, '')}`)
17+
.join('_')
18+
}
1419
const key = Symbol.for(uniqueTypeName)
1520
let constraintType = constraintTypes[key]
1621

@@ -86,6 +91,7 @@ const constraintDirectiveTypeDefs = `
8691
exclusiveMin: Int
8792
exclusiveMax: Int
8893
multipleOf: Int
94+
uniqueTypeName: String
8995
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION`
9096

9197
module.exports = { constraintDirective, constraintDirectiveTypeDefs }

test/int.test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,65 @@ describe('@constraint Int in INPUT_FIELD_DEFINITION', function () {
355355
'Variable "$input" got invalid value {}; Field "title" of required type "title_Int_NotNull_multipleOf_2!" was not provided.')
356356
})
357357
})
358+
359+
describe('#uniqueTypeName', function () {
360+
before(function () {
361+
this.typeDefs = `
362+
type Query {
363+
books: [Book]
364+
}
365+
type Book {
366+
title: String
367+
}
368+
type Mutation {
369+
createBook(input: BookInput): Book
370+
}
371+
input BookInput {
372+
title: Int! @constraint(min: 3, uniqueTypeName: "BookInput_Title")
373+
}`
374+
375+
this.request = setup(this.typeDefs)
376+
})
377+
378+
it('should pass', async function () {
379+
const { body, statusCode } = await this.request
380+
.post('/graphql')
381+
.set('Accept', 'application/json')
382+
.send({ query, variables: { input: { title: 3 } }
383+
})
384+
385+
strictEqual(statusCode, 200)
386+
deepStrictEqual(body, { data: { createBook: null } })
387+
})
388+
389+
it('should fail', async function () {
390+
const { body, statusCode } = await this.request
391+
.post('/graphql')
392+
.set('Accept', 'application/json')
393+
.send({ query, variables: { input: { title: 2 } }
394+
})
395+
396+
strictEqual(statusCode, 400)
397+
strictEqual(body.errors[0].message,
398+
'Variable "$input" got invalid value 2 at "input.title"; Expected type "BookInput_Title". Must be at least 3')
399+
})
400+
401+
it('should throw custom error', async function () {
402+
const request = setup(this.typeDefs, formatError)
403+
const { body, statusCode } = await request
404+
.post('/graphql')
405+
.set('Accept', 'application/json')
406+
.send({ query, variables: { input: { title: 2 } } })
407+
408+
strictEqual(statusCode, 400)
409+
deepStrictEqual(body.errors[0], {
410+
message: 'Must be at least 3',
411+
code: 'ERR_GRAPHQL_CONSTRAINT_VALIDATION',
412+
fieldName: 'title',
413+
context: [{ arg: 'min', value: 3 }]
414+
})
415+
})
416+
})
358417
})
359418

360419
describe('@constraint Int in FIELD_DEFINITION', function () {
@@ -790,4 +849,58 @@ describe('@constraint Int in FIELD_DEFINITION', function () {
790849
})
791850
})
792851
})
852+
853+
describe('#uniqueTypeName', function () {
854+
before(function () {
855+
this.typeDefs = `
856+
type Query {
857+
books: [Book]
858+
}
859+
type Book {
860+
title: Int @constraint(min: 2, uniqueTypeName: "Book_Title")
861+
}
862+
`
863+
})
864+
865+
it('should pass', async function () {
866+
const mockData = [{title: 2}, {title: 3}]
867+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
868+
const { body, statusCode } = await request
869+
.post('/graphql')
870+
.set('Accept', 'application/json')
871+
.send({ query })
872+
873+
strictEqual(statusCode, 200)
874+
deepStrictEqual(body, { data: { books: mockData } })
875+
})
876+
877+
it('should fail', async function () {
878+
const mockData = [{title: 1}, {title: 2}]
879+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
880+
const { body, statusCode } = await request
881+
.post('/graphql')
882+
.set('Accept', 'application/json')
883+
.send({ query })
884+
885+
strictEqual(statusCode, 200)
886+
strictEqual(body.errors[0].message, 'Must be at least 2')
887+
})
888+
889+
it('should throw custom error', async function () {
890+
const mockData = [{title: 1}, {title: 2}]
891+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
892+
const { body, statusCode } = await request
893+
.post('/graphql')
894+
.set('Accept', 'application/json')
895+
.send({ query })
896+
897+
strictEqual(statusCode, 200)
898+
deepStrictEqual(body.errors[0], {
899+
message: 'Must be at least 2',
900+
code: 'ERR_GRAPHQL_CONSTRAINT_VALIDATION',
901+
fieldName: 'title',
902+
context: [{ arg: 'min', value: 2 }]
903+
})
904+
})
905+
})
793906
})

test/introspection.test.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { strictEqual } = require('assert')
1+
const { strictEqual, notEqual } = require('assert')
22
const { getIntrospectionQuery } = require('graphql')
33
const setup = require('./setup')
44

@@ -16,7 +16,7 @@ describe('Introspection', function () {
1616
}
1717
input BookInput {
1818
title: String! @constraint(minLength: 3 maxLength: 5)
19-
subTitle: Int! @constraint(max: 3)
19+
subTitle: Int! @constraint(max: 3, uniqueTypeName: "BookInput_subTitle")
2020
}`
2121

2222
this.request = setup(this.typeDefs)
@@ -29,9 +29,19 @@ describe('Introspection', function () {
2929
.send({ query: getIntrospectionQuery() })
3030

3131
strictEqual(statusCode, 200)
32-
3332
const directive = body.data.__schema.directives.find(v => v.name === 'constraint')
33+
strictEqual(directive.args.length, 14)
34+
35+
const type = body.data.__schema.types.find(t => t.name === 'BookInput_subTitle')
36+
notEqual(type, null)
37+
})
38+
it('should allow unique type names to be added', async function () {
39+
const { body } = await this.request
40+
.post('/graphql')
41+
.set('Accept', 'application/json')
42+
.send({ query: getIntrospectionQuery() })
3443

35-
strictEqual(directive.args.length, 13)
44+
const type = body.data.__schema.types.find(t => t.name === 'BookInput_subTitle')
45+
notEqual(type, null)
3646
})
3747
})

test/string.test.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,65 @@ describe('@constraint String in INPUT_FIELD_DEFINITION', function () {
10071007
'Variable "$input" got invalid value {}; Field "title" of required type "title_String_NotNull_minLength_3!" was not provided.')
10081008
})
10091009
})
1010+
1011+
describe('#uniqueTypeName', function () {
1012+
before(function () {
1013+
this.typeDefs = `
1014+
type Query {
1015+
books: [Book]
1016+
}
1017+
type Book {
1018+
title: String
1019+
}
1020+
type Mutation {
1021+
createBook(input: BookInput): Book
1022+
}
1023+
input BookInput {
1024+
title: String! @constraint(minLength: 3, uniqueTypeName: "BookInput_Title")
1025+
}`
1026+
1027+
this.request = setup(this.typeDefs)
1028+
})
1029+
1030+
it('should pass', async function () {
1031+
const { body, statusCode } = await this.request
1032+
.post('/graphql')
1033+
.set('Accept', 'application/json')
1034+
.send({ query, variables: { input: { title: 'he💩' } }
1035+
})
1036+
1037+
strictEqual(statusCode, 200)
1038+
deepStrictEqual(body, { data: { createBook: null } })
1039+
})
1040+
1041+
it('should fail', async function () {
1042+
const { body, statusCode } = await this.request
1043+
.post('/graphql')
1044+
.set('Accept', 'application/json')
1045+
.send({ query, variables: { input: { title: 'a💩' } }
1046+
})
1047+
1048+
strictEqual(statusCode, 400)
1049+
strictEqual(body.errors[0].message,
1050+
'Variable "$input" got invalid value "a💩" at "input.title"; Expected type "BookInput_Title". Must be at least 3 characters in length')
1051+
})
1052+
1053+
it('should throw custom error', async function () {
1054+
const request = setup(this.typeDefs, formatError)
1055+
const { body, statusCode } = await request
1056+
.post('/graphql')
1057+
.set('Accept', 'application/json')
1058+
.send({ query, variables: { input: { title: 'a💩' } } })
1059+
1060+
strictEqual(statusCode, 400)
1061+
deepStrictEqual(body.errors[0], {
1062+
message: 'Must be at least 3 characters in length',
1063+
code: 'ERR_GRAPHQL_CONSTRAINT_VALIDATION',
1064+
fieldName: 'title',
1065+
context: [{ arg: 'minLength', value: 3 }]
1066+
})
1067+
})
1068+
})
10101069
})
10111070

10121071
describe('@constraint String in FIELD_DEFINITION', function () {
@@ -1862,4 +1921,57 @@ describe('@constraint String in FIELD_DEFINITION', function () {
18621921
})
18631922
})
18641923
})
1924+
1925+
describe('#uniqueTypeName', function () {
1926+
before(function () {
1927+
this.typeDefs = `
1928+
type Query {
1929+
books: [Book]
1930+
}
1931+
type Book {
1932+
title: String @constraint(minLength: 3, uniqueTypeName: "Book_Title")
1933+
}`
1934+
})
1935+
1936+
it('should pass', async function () {
1937+
const mockData = [{title: 'foo'}, {title: 'foobar'}]
1938+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
1939+
const { body, statusCode } = await request
1940+
.post('/graphql')
1941+
.set('Accept', 'application/json')
1942+
.send({ query })
1943+
1944+
strictEqual(statusCode, 200)
1945+
deepStrictEqual(body, { data: { books: mockData } })
1946+
})
1947+
1948+
it('should fail', async function () {
1949+
const mockData = [{title: 'fo'}, {title: 'foo'}]
1950+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
1951+
const { body, statusCode } = await request
1952+
.post('/graphql')
1953+
.set('Accept', 'application/json')
1954+
.send({ query })
1955+
1956+
strictEqual(statusCode, 200)
1957+
strictEqual(body.errors[0].message, 'Must be at least 3 characters in length')
1958+
})
1959+
1960+
it('should throw custom error', async function () {
1961+
const mockData = [{title: 'fo'}, {title: 'foo'}]
1962+
const request = setup(this.typeDefs, formatError, resolvers(mockData))
1963+
const { body, statusCode } = await request
1964+
.post('/graphql')
1965+
.set('Accept', 'application/json')
1966+
.send({ query })
1967+
1968+
strictEqual(statusCode, 200)
1969+
deepStrictEqual(body.errors[0], {
1970+
message: 'Must be at least 3 characters in length',
1971+
code: 'ERR_GRAPHQL_CONSTRAINT_VALIDATION',
1972+
fieldName: 'title',
1973+
context: [{ arg: 'minLength', value: 3 }]
1974+
})
1975+
})
1976+
})
18651977
})

0 commit comments

Comments
 (0)