graphql-constraint-directive
Advanced tools
Comparing version 5.0.0 to 5.1.0
@@ -1,6 +0,19 @@ | ||
import {GraphQLSchema} from "graphql"; | ||
import {DocumentNode, GraphQLSchema} from "graphql"; | ||
import {ApolloServerPlugin} from '@apollo/server'; | ||
export const constraintDirectiveTypeDefs: string | ||
/** | ||
* Constraint directive typeDef as a `string` | ||
*/ | ||
export const constraintDirectiveTypeDefs: string; | ||
export function createApollo4QueryValidationPlugin ( options: { schema: GraphQLSchema } ) : ApolloServerPlugin; | ||
/** | ||
* Constraint directive typeDef as a `DocumentNode` | ||
*/ | ||
export const constraintDirectiveTypeDefsGql: DocumentNode; | ||
/** | ||
* Create Apollo 4 validation plugin. | ||
* | ||
* @param options to setup plugin. `schema` is deprecated now, not used, as plugins gets schema from the Apollo Server. | ||
*/ | ||
export function createApollo4QueryValidationPlugin ( options: { schema?: GraphQLSchema } ) : ApolloServerPlugin; |
const { | ||
separateOperations, | ||
buildSchema, | ||
GraphQLError | ||
@@ -9,4 +10,13 @@ } = require('graphql') | ||
function createApollo4QueryValidationPlugin ({ schema }) { | ||
let currentSchema | ||
function createApollo4QueryValidationPlugin () { | ||
return { | ||
async serverWillStart () { | ||
return { | ||
schemaDidLoadOrUpdate ({ apiSchema, coreSupergraphSdl }) { | ||
if (coreSupergraphSdl) { currentSchema = buildSchema(coreSupergraphSdl) } else { currentSchema = apiSchema } | ||
} | ||
} | ||
}, | ||
async requestDidStart () { | ||
@@ -21,3 +31,3 @@ return ({ | ||
const errors = validateQuery( | ||
schema, | ||
currentSchema, | ||
query, | ||
@@ -24,0 +34,0 @@ request.variables, |
@@ -5,12 +5,66 @@ import {GraphQLSchema, GraphQLError, DocumentNode, ValidationContext} from "graphql"; | ||
/** | ||
* Schema transformer which adds custom types performing validations based on the @constraint directives. | ||
*/ | ||
export function constraintDirective () : (schema: GraphQLSchema) => GraphQLSchema; | ||
interface DocumentationOptions { | ||
/** Header for the constraints documentation block in the field or argument description */ | ||
header?: string; | ||
/** Names for distinct constraint types */ | ||
descriptionsMap?: { | ||
minLength: string, | ||
maxLength: string, | ||
startsWith: string, | ||
endsWith: string, | ||
contains: string, | ||
notContains: string, | ||
pattern: string, | ||
format: string, | ||
min: string, | ||
max: string, | ||
exclusiveMin: string, | ||
exclusiveMax: string, | ||
multipleOf: string, | ||
minItems: string, | ||
maxItems: string | ||
}; | ||
} | ||
/** | ||
* Schema transformer which adds @constraint directives documentation to the fields and arguments descriptions. | ||
* Documentation not added if it already exists (`header` is present in the field or argument description) | ||
* | ||
* @param options options to customize the documentation process | ||
*/ | ||
export function constraintDirectiveDocumentation (options: DocumentationOptions) : (schema: GraphQLSchema) => GraphQLSchema; | ||
/** | ||
* Type definition for @constraint directive. | ||
*/ | ||
export const constraintDirectiveTypeDefs: string | ||
/** | ||
* Method for query validation based on the @constraint directives defined in the schema. | ||
* | ||
* @param schema GraphQL schema to look for directives | ||
* @param query GraphQL query to validate | ||
* @param variables used in the query to validate | ||
* @param operationName optional name of the GraphQL operation to validate | ||
*/ | ||
export function validateQuery () : (schema: GraphQLSchema, query: DocumentNode, variables: Record<string, any>, operationName?: string) => Array<GraphQLError>; | ||
/** | ||
* Create Apollo 3 plugin performing query validation. | ||
*/ | ||
export function createApolloQueryValidationPlugin ( options: { schema: GraphQLSchema } ) : PluginDefinition; | ||
/** | ||
* Create JS GraphQL Validation Rule performing query validation. | ||
*/ | ||
export function createQueryValidationRule( options: { [key: string]: any }) : (context: ValidationContext) => QueryValidationVisitor; | ||
/** | ||
* Create Envelop plugin performing query validation. | ||
*/ | ||
export function createEnvelopQueryValidationPlugin() : object; |
78
index.js
@@ -9,3 +9,4 @@ const { | ||
separateOperations, | ||
GraphQLError | ||
GraphQLError, | ||
getDirectiveValues | ||
} = require('graphql') | ||
@@ -15,3 +16,3 @@ const QueryValidationVisitor = require('./lib/query-validation-visitor.js') | ||
const { getConstraintTypeObject, getScalarType } = require('./lib/type-utils') | ||
const { constraintDirectiveTypeDefs } = require('./lib/type-defs') | ||
const { constraintDirectiveTypeDefs, constraintDirectiveTypeDefsObj } = require('./lib/type-defs') | ||
@@ -100,2 +101,73 @@ function constraintDirective () { | ||
function constraintDirectiveDocumentation (options) { | ||
// Default descriptions, can be changed through options | ||
let DESCRIPTINS_MAP = { | ||
minLength: 'Minimal length', | ||
maxLength: 'Maximal length', | ||
startsWith: 'Starts with', | ||
endsWith: 'Ends with', | ||
contains: 'Contains', | ||
notContains: 'Doesn\'t contain', | ||
pattern: 'Must match RegEx pattern', | ||
format: 'Must match format', | ||
min: 'Minimal value', | ||
max: 'Maximal value', | ||
exclusiveMin: 'Grater than', | ||
exclusiveMax: 'Less than', | ||
multipleOf: 'Must be a multiple of', | ||
minItems: 'Minimal number of items', | ||
maxItems: 'Maximal number of items' | ||
} | ||
if (options?.descriptionsMap) { | ||
DESCRIPTINS_MAP = options.descriptionsMap | ||
} | ||
let HEADER = '*Constraints:*' | ||
if (options?.header) { | ||
HEADER = options.header | ||
} | ||
function documentConstraintDirective (fieldConfig, directiveArgumentMap) { | ||
if (fieldConfig.description) { | ||
// skip documentation if it is already here | ||
if (fieldConfig.description.includes(HEADER)) return | ||
// add two new lines to separate from previous description by paragraph | ||
fieldConfig.description += '\n\n' | ||
} else { | ||
fieldConfig.description = '' | ||
} | ||
fieldConfig.description += HEADER + '\n' | ||
Object.entries(directiveArgumentMap).forEach(([key, value]) => { | ||
if (key === 'uniqueTypeName') return | ||
fieldConfig.description += `* ${DESCRIPTINS_MAP[key] ? DESCRIPTINS_MAP[key] : key}: \`${value}\`\n` | ||
}) | ||
} | ||
return (schema) => | ||
mapSchema(schema, { | ||
[MapperKind.FIELD]: (fieldConfig) => { | ||
const directiveArgumentMap = getDirectiveValues(constraintDirectiveTypeDefsObj, fieldConfig.astNode) | ||
if (directiveArgumentMap) { | ||
documentConstraintDirective(fieldConfig, directiveArgumentMap) | ||
return fieldConfig | ||
} | ||
}, | ||
[MapperKind.ARGUMENT]: (fieldConfig) => { | ||
const directiveArgumentMap = getDirectiveValues(constraintDirectiveTypeDefsObj, fieldConfig.astNode) | ||
if (directiveArgumentMap) { | ||
documentConstraintDirective(fieldConfig, directiveArgumentMap) | ||
return fieldConfig | ||
} | ||
} | ||
}) | ||
} | ||
function validateQuery (schema, query, variables, operationName) { | ||
@@ -165,2 +237,2 @@ const typeInfo = new TypeInfo(schema) | ||
module.exports = { constraintDirective, constraintDirectiveTypeDefs, validateQuery, createApolloQueryValidationPlugin, createEnvelopQueryValidationPlugin, createQueryValidationRule } | ||
module.exports = { constraintDirective, constraintDirectiveDocumentation, constraintDirectiveTypeDefs, validateQuery, createApolloQueryValidationPlugin, createEnvelopQueryValidationPlugin, createQueryValidationRule } |
{ | ||
"name": "graphql-constraint-directive", | ||
"version": "5.0.0", | ||
"version": "5.1.0", | ||
"description": "Validate GraphQL fields", | ||
@@ -9,2 +9,3 @@ "main": "index.js", | ||
"test": "standard && nyc --reporter=html --reporter=text --reporter=lcov mocha test/**/testsuite-full.js", | ||
"test-documentation": "standard && nyc --reporter=html --reporter=text --reporter=lcov mocha test/**/testsuite-documentation.js", | ||
"test-schema-wrapper": "standard && nyc --reporter=html --reporter=text --reporter=lcov mocha test/**/testsuite-schema-wrapper.js", | ||
@@ -37,4 +38,4 @@ "test-apollo-plugin": "standard && nyc --reporter=html --reporter=text --reporter=lcov mocha test/**/testsuite-apollo-plugin.js", | ||
"devDependencies": { | ||
"apollo-server-express": "3.11.1", | ||
"@apollo/server": "4.3.2", | ||
"apollo-server-express": "3.12.0", | ||
"@apollo/server": "4.5.0", | ||
"coveralls": "3.1.1", | ||
@@ -41,0 +42,0 @@ "express": "4.18.2", |
129
README.md
@@ -25,6 +25,6 @@ # graphql-constraint-directive | ||
Implementation based on schema wrappers - basic scalars are wrapped as custom scalars with validations. | ||
Implementation based on schema wrappers - basic scalars are wrapped as custom scalars with validations. | ||
#### Benefits | ||
* based on `graphql` library, works everywhere | ||
* based on `graphql` library, works everywhere | ||
* posibility to also validate GraphQL response data | ||
@@ -85,2 +85,5 @@ | ||
This plugin requires the following dependencies installed in your project: | ||
* `@envelop/core` - `^2.0.0` | ||
```js | ||
@@ -127,2 +130,5 @@ const { createEnvelopQueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive') | ||
This plugin requires the following dependencies installed in your project: | ||
* dependencies required for your selected Apollo Server 3 variant | ||
```js | ||
@@ -159,3 +165,3 @@ const { createApolloQueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive') | ||
const app = express() | ||
const server = new ApolloServer({ | ||
const server = new ApolloServer({ | ||
schema, | ||
@@ -174,9 +180,11 @@ plugins | ||
This plugin requires the following dependencies installed in your project: | ||
* `@apollo/server` - `^4.0.0` | ||
* `graphql-tag` - `^2.0.0` | ||
```js | ||
const { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive/apollo4') | ||
const express = require('express') | ||
const { ApolloServer } = require('@apollo/server') | ||
const { startStandaloneServer } = require('@apollo/server/standalone'); | ||
const { makeExecutableSchema } = require('@graphql-tools/schema') | ||
const cors = require('cors') | ||
const { json } = require('body-parser') | ||
@@ -202,9 +210,6 @@ const typeDefs = ` | ||
const plugins = [ | ||
createApollo4QueryValidationPlugin({ | ||
schema | ||
}) | ||
createApollo4QueryValidationPlugin() | ||
] | ||
const app = express() | ||
const server = new ApolloServer({ | ||
const server = new ApolloServer({ | ||
schema, | ||
@@ -214,10 +219,3 @@ plugins | ||
await server.start() | ||
app.use( | ||
'/', | ||
cors(), | ||
json(), | ||
expressMiddleware(server) | ||
) | ||
await startStandaloneServer(server); | ||
``` | ||
@@ -229,2 +227,6 @@ #### Apollo 4 Subgraph server | ||
This plugin requires the following dependencies installed in your project: | ||
* `@apollo/server` - `^4.0.0` | ||
* `graphql-tag` - `^2.0.0` | ||
```ts | ||
@@ -258,5 +260,3 @@ import { ApolloServer } from '@apollo/server'; | ||
const plugins = [ | ||
createApollo4QueryValidationPlugin({ | ||
schema | ||
}) | ||
createApollo4QueryValidationPlugin() | ||
] | ||
@@ -274,5 +274,5 @@ | ||
*This implementation is untested now, as [`express-graphql` module](https://github.com/graphql/express-graphql) is not maintained anymore.* | ||
*This implementation is untested now, as [`express-graphql` module](https://github.com/graphql/express-graphql) is not maintained anymore.* | ||
As a [Validation rule](https://graphql.org/graphql-js/validation/) when query `variables` are available | ||
As a [Validation rule](https://graphql.org/graphql-js/validation/) when query `variables` are available | ||
@@ -319,3 +319,78 @@ ```js | ||
``` | ||
### Schema documentation | ||
You can use the provided schema transformation to automatically add `@constraint` documentation into fields and arguments descriptions. By default directives are not typically present in the exposed introspected schema | ||
```js | ||
const { constraintDirectiveTypeDefs, constraintDirectiveDocumentation } = require('graphql-constraint-directive') | ||
const { makeExecutableSchema } = require('@graphql-tools/schema') | ||
const typeDefs = ... | ||
let schema = makeExecutableSchema({ | ||
typeDefs: [constraintDirectiveTypeDefs, typeDefs] | ||
}) | ||
schema = constraintDirectiveDocumentation()(schema); | ||
// any constraint directive handler implementation | ||
``` | ||
This transformation appends `constraint documentation header`, and then a list of `constraint conditions descriptions` to the description of each field and argument where the `@constraint` directive is used. | ||
Original schema: | ||
```graphql | ||
""" | ||
Existing field or argument description. | ||
""" | ||
fieldOrArgument: String @constraint(minLength: 10, maxLength: 50) | ||
``` | ||
Transformed schema: | ||
```graphql | ||
""" | ||
Existing field or argument description. | ||
*Constraints:* | ||
* Minimum length: `10` | ||
* Maximum length: `50` | ||
""" | ||
fieldOrArgument: String @constraint(minLength: 10, maxLength: 50) | ||
``` | ||
[CommonMark](https://spec.commonmark.org) is used in the desccription for better readability. | ||
If `constraint documentation header` already exists in the field or argument description, then | ||
constraint documentation is not appended. This allows you to override constraint description | ||
when necessary, or use this in a chain of subgraph/supergraph schemes. | ||
Both `constraint documentation header` and `constraint conditions descriptions` can be customized | ||
during the transformation creation, eg. to localize them. | ||
```js | ||
schema = constraintDirectiveDocumentation( | ||
{ | ||
header: '*Changed header:*', | ||
descriptionsMap: { | ||
minLength: 'Changed Minimum length', | ||
maxLength: 'Changed Maximum length', | ||
startsWith: 'Changed Starts with', | ||
endsWith: 'Changed Ends with', | ||
contains: 'Changed Contains', | ||
notContains: 'Changed Doesn\'t contain', | ||
pattern: 'Changed Must match RegEx pattern', | ||
format: 'Changed Must match format', | ||
min: 'Changed Minimum value', | ||
max: 'Changed Maximum value', | ||
exclusiveMin: 'Changed Grater than', | ||
exclusiveMax: 'Changed Less than', | ||
multipleOf: 'Changed Must be a multiple of', | ||
minItems: 'Changed Minimum number of items', | ||
maxItems: 'Changed Maximum number of items' | ||
} | ||
} | ||
)(schema); | ||
``` | ||
## API | ||
@@ -425,4 +500,4 @@ ### String | ||
#### Apollo Server 4 | ||
Throws a prefilled `GraphQLError` with `extensions.code` set to `BAD_USER_INPUT` and http status code `400`. | ||
In case of more validation errors, top level error is generic with `Query is invalid, for details see extensions.validationErrors` message, | ||
Throws a prefilled `GraphQLError` with `extensions.code` set to `BAD_USER_INPUT` and http status code `400`. | ||
In case of more validation errors, top level error is generic with `Query is invalid, for details see extensions.validationErrors` message, | ||
detailed errors are stored in `extensions.validationErrors` of this error. | ||
@@ -435,3 +510,3 @@ | ||
```@constraint(uniqueTypeName: "Unique_Type_Name")``` | ||
Override the unique type name generate by the library to the one passed as an argument. | ||
Override the unique type name generate by the library to the one passed as an argument. | ||
Has meaning only for `Schema wrapper` implementation. |
@@ -21,5 +21,3 @@ const express = require('express') | ||
const plugins = [ | ||
createApollo4QueryValidationPlugin({ | ||
schema | ||
}) | ||
createApollo4QueryValidationPlugin() | ||
] | ||
@@ -26,0 +24,0 @@ |
@@ -6,1 +6,2 @@ require('./testsuite-schema-wrapper') | ||
// require('./testsuite-validation-rule-express-graphql') // deprecated, may be removed later or rewritent to another server if available | ||
require('./testsuite-documentation') |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
289635
57
6848
500
1