Skip to content

Commit

Permalink
implement imperative configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ivandotv committed Jan 3, 2022
1 parent fccb773 commit dc1d95f
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 22 deletions.
32 changes: 32 additions & 0 deletions .changeset/angry-rats-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'graphql-no-alias': major
---

Implement imperative configuration

With imperative configuration, there is no need for type definition and schema modification.

```ts
const permissions = {
Query: {
'*': 2, // default value for all queries
getAnotherUser: 5 // custom value for specific query
},
Mutation: {
'*': 1 //default value for all mutations
}
}
const { validation } = createValidation({ permissions })

const schema = buildSchema(/* GraphQL */ `
type Query {
getUser: User
getAnotherUser: User
}
type User {
name: String
}
`)
```

When the `permissions` key is passed to configuration, schema directives will be ignored.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ npm i graphql-no-alias

## Usage

There are two parts, a `directive` declaration that needs to be added to the schema, and a validation function that needs to be added to the `GraphQl` `validationRules` array.
There are two ways to use this validation:

- Using the `directive` in the `schema`
- [Using the configuration options](#Imperative-configuration)

### Using the directive

There are two parts, a `directive` that needs to be added to the `schema`, and a validation function that needs to be added to the `GraphQl` `validationRules` array.

```js
const express = require('express')
Expand Down Expand Up @@ -157,7 +164,7 @@ On the client:

### Customizing the declaration

The declaration can be customized to have a different name, different default `allow` value, and it can also be passed a custom error function that is executed when the validation fails.
The declaration can be customized to have a different name, different default `allow` values, and it can also be passed a custom error function that is executed when the validation fails.

In the next example, `validation` will allow `3` calls to the same field by default, the directive name will be changed to `NoBatchCalls`, and there will be a custom error message.

Expand All @@ -182,6 +189,35 @@ const schema = buildSchema(`
`)
```

### Imperative configuration

With imperative configuration, there is no need for type definition and schema modification.

```ts
const permissions = {
Query: {
'*': 2, // default value for all queries
getAnotherUser: 5 // custom value for specific query
},
Mutation: {
'*': 1 //default value for all mutations
}
}
const { validation } = createValidation({ permissions })
const schema = buildSchema(/* GraphQL */ `
type Query {
getUser: User
getAnotherUser: User
}
type User {
name: String
}
`)
```

When the `permissions` key is passed to configuration, schema directives will be ignored.

### Customizing the error message

Continuing from the previous example, the `error` message that is reported when the validation fails can also be customized. You can return a complete `GrahphQLError` or just a `string` that will be used as a message.
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Directive on type field', () => {

const query = /* GraphQL */ `
{
getUser @noAlias {
getUser {
name
}
alias_1: getUser {
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('Directive on type field', () => {
)
})

test('Set custom number of allowd aliases', () => {
test('Set custom number of allowed aliases', () => {
const { validation, typeDefs } = createValidation()
const allow = 3

Expand All @@ -80,7 +80,7 @@ describe('Directive on type field', () => {
`)

const query = /* GraphQL */ `
{
query Test {
getUser {
name
}
Expand All @@ -105,7 +105,7 @@ describe('Directive on type field', () => {
)
})

test('Set default custom maximum allowed when creating the validation', () => {
test('Set custom default value maximum allowed when creating the validation', () => {
const defaultAllow = 3
const { validation, typeDefs } = createValidation({ defaultAllow })

Expand Down Expand Up @@ -192,7 +192,7 @@ describe('Directive on type field', () => {
)
})

test('Report one error per field', () => {
test('Always report single error per field', () => {
const defaultAllow = 3
const directiveName = 'customDirectiveName'

Expand Down Expand Up @@ -247,7 +247,7 @@ describe('Directive on type field', () => {
})

describe('Custom error', () => {
test('Return custom graphql error', () => {
test('Return custom GraphQlError instance', () => {
const allow = 1
const errorMessage = 'custom_error_message'

Expand Down Expand Up @@ -283,7 +283,7 @@ describe('Directive on type field', () => {
expect(errors[0].message).toMatch(errorMessage)
})

test('Return custom error string', () => {
test('Return string from error', () => {
const allow = 1
const errorMessage = 'custom_error_message'

Expand Down
80 changes: 80 additions & 0 deletions src/__tests__/permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { buildSchema, GraphQLError, parse, validate } from 'graphql'
import createValidation from '../'

describe('Permissions via config', () => {
test('Set default value for all queries', () => {
const permissions = {
Query: {
'*': 1
}
}
const { validation } = createValidation({ permissions })

const schema = buildSchema(/* GraphQL */ `
type Query {
getUser: User
getAnotherUser: User
}
type User {
name: String
}
`)

const query = /* GraphQL */ `
{
getUser {
name
}
alias_1: getUser {
name
}
getAnotherUser {
name
}
alias_1: getAnotherUser {
name
}
}
`
const doc = parse(query)
const errors = validate(schema, doc, [validation])
expect(errors).toHaveLength(2)
})

test('Override default value for specific query call', () => {
const permissions = {
Query: {
'*': 1,
getAnotherUser: 2
}
}
const { validation } = createValidation({ permissions })

const schema = buildSchema(/* GraphQL */ `
type Query {
getUser: User
getAnotherUser: User
}
type User {
name: String
}
`)

const query = /* GraphQL */ `
{
getUser {
name
}
getAnotherUser {
name
}
alias_1: getAnotherUser {
name
}
}
`
const doc = parse(query)
const errors = validate(schema, doc, [validation])
expect(errors).toHaveLength(0)
})
})
71 changes: 58 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export type ErrorFn = typeof createErrorMsg
/**
* Configuration object for the createValidation function
*/
type Permissions = { [key: string]: Permissions | number }
export type Config = {
permissions?: Permissions
/** How many aliases (calls) to allow by default */
defaultAllow?: number
/** directive name to use*/
Expand All @@ -31,7 +33,7 @@ export default function createValidation(config?: Config): {
typeDefs: string
validation: (context: ValidationContext) => ASTVisitor
} {
const { directiveName, defaultAllow, errorFn } = {
const { directiveName, defaultAllow, errorFn, permissions } = {
...{
defaultAllow: 1,
directiveName: 'noAlias',
Expand All @@ -49,7 +51,8 @@ export default function createValidation(config?: Config): {
context,
directiveName,
defaultAllow,
errorFn
errorFn,
permissions
)
}
}
Expand All @@ -59,18 +62,57 @@ export default function createValidation(config?: Config): {
}
}

function configPermissionWalker(
permissions: Permissions,
result: Map<string, number>,
parentKey?: string
): void {
Object.entries(permissions).forEach(([key, value]) => {
if (typeof value === 'object') {
configPermissionWalker(
value,
result,
`${parentKey ? parentKey : ''}${parentKey && key ? '.' : ''}${
key ? key : ''
}`
)
} else {
if (key === '*') {
result.set(parentKey!, value)
} else {
result.set(`${parentKey ? parentKey : ''}.${key}`, value)
}
}
})
}

function buildPermissionTableFromConfig(permissions: any): Map<string, number> {
const result = new Map()
configPermissionWalker(permissions, result, undefined)

return result
}

function createFieldValidation(
context: ValidationContext,
directiveName: string,
defaultAllow: number,
errorFn: typeof createErrorMsg
errorFn: ErrorFn,
permissions?: Permissions
): (node: FieldNode) => void {
const schema = context.getSchema()

const allowedCount = createMaxAllowedTable(defaultAllow, directiveName, [
schema.getQueryType(),
schema.getMutationType()
])
let allowedCount: Map<string, number>

if (permissions) {
allowedCount = buildPermissionTableFromConfig(permissions)
} else {
allowedCount = buildPermissionTableFromSchema(defaultAllow, directiveName, [
schema.getQueryType(),
schema.getMutationType()
])
}

const currentCount: Map<string, number> = new Map()
//track if the error have already been reported for particular field
const errorMap: Map<string, boolean> = new Map()
Expand All @@ -97,8 +139,8 @@ function checkCount(
): void {
const nodeName = node.name.value
const typeName = ctx.getParentType()!.name
const fieldKey = `${typeName}-${nodeName}`
const typeKey = `${typeName}`
const fieldKey = `${typeKey}.${nodeName}`
const maxAllowed = maxAllowedData.get(fieldKey) || maxAllowedData.get(typeKey)

if (maxAllowed) {
Expand Down Expand Up @@ -127,7 +169,7 @@ function checkCount(
* Process appropriate schema types (Query, Mutation) and resolve all directive values by
* building a mapping between type fields and allowed values
*/
function createMaxAllowedTable(
function buildPermissionTableFromSchema(
defaultAllow: number,
directiveName: string,
types: (GraphQLObjectType | undefined | null)[]
Expand All @@ -145,7 +187,7 @@ function createMaxAllowedTable(
: undefined

if (value) {
maxAllowed.set(`${graphType?.name}`, value)
maxAllowed.set(`${graphType!.name}`, value)
}

if (graphType?.astNode?.fields) {
Expand All @@ -157,7 +199,7 @@ function createMaxAllowedTable(
field.directives
)
if (value) {
maxAllowed.set(`${graphType}-${field.name.value}`, value)
maxAllowed.set(`${graphType}.${field.name.value}`, value)
}
}
}
Expand Down Expand Up @@ -193,8 +235,11 @@ function createErrorMsg(
typeName: string,
fieldName: string,
maxAllowed: number,
_node: FieldNode,
node: FieldNode,
_ctx: ValidationContext
): GraphQLError | string {
return `Allowed number of calls for ${typeName}->${fieldName} has been exceeded (max: ${maxAllowed})`
return new GraphQLError(
`Allowed number of calls for ${typeName}->${fieldName} has been exceeded (max: ${maxAllowed})`,
node
)
}

0 comments on commit dc1d95f

Please sign in to comment.