graphql-introspection-filtering
Advanced tools
Comparing version
@@ -16,3 +16,2 @@ "use strict"; | ||
Object.defineProperty(exports, "IntrospectionMapperKind", { enumerable: true, get: function () { return types_1.IntrospectionMapperKind; } }); | ||
// todo docs | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "graphql-introspection-filtering", | ||
"version": "3.0.0-beta", | ||
"version": "3.0.0", | ||
"description": "Filter graphql schema introspection result to hide restricted fields and types", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
270
README.md
@@ -32,6 +32,7 @@ # graphql-introspection-filtering | ||
Filtering is possible on schemas created with `makeExecutableSchema`, provided by `graphql-introspection-filtering`. | ||
To enable filtering, a mapper has to be applied to schema. | ||
``` | ||
import makeExecutableSchema from 'graphql-introspection-filtering'; | ||
import makeExecutableSchema, { mapSchema } from 'graphql-introspection-filtering'; | ||
const schema = makeExecutableSchema(schemaConfig[, builder]); | ||
export default mapSchema(makeExecutableSchema(schemaConfig[, builder]), mapper); | ||
``` | ||
@@ -43,81 +44,68 @@ | ||
| Option | Type | Default | Description | | ||
|---------------------|------------------------------------------|---------|-------------| | ||
| Option | Type | Default | Description | | ||
|---------------------|------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| **shouldSkipQuery** | `null`, `number`, `(context) => boolean` | `null` | When positive number provided, this number of introspection queries will be unfiltered. Alternatively callback can be provided, it takes `context` as an argument, and should return boolean. | | ||
| **hookDirectives** | `boolean`, `string[]` | `true` | Whether to hook directives, array of specific directive names can be provided | | ||
- `builder` - builder function (default: original graphql `makeExecutableSchema`) | ||
- `mapper` - introspection schema mapper | ||
### Create introspection schema visitor | ||
### Create introspection schema mapper | ||
Every object and field is visited by a directive visitor, where corresponding directive is applied on it in | ||
a schema definition (directives on directives are not allowed, so all directives pass this requirement) | ||
**AND** assigned directive visitor contains a corresponding introspection visitor method. | ||
a schema definition (directives configured separately) **AND** applied mapper contains a | ||
corresponding introspection visitor method. | ||
Directive visitor instance is created for every object and field it was applied to (and all directive definitions). | ||
When *falsy* value is returned by a mapped resolver, the field / object is excluded from introspection result. | ||
When *falsy* value is returned by a visitor, the field / object is excluded from introspection result. | ||
Example introspection mapper can be found below. | ||
Example introspection directive visitor below. | ||
```ts | ||
class AuthenticationDirective<TArgs = any, TContext = any> extends SchemaDirectiveVisitor<TArgs, TContext> implements IntrospectionDirectiveVisitor<TArgs, TContext> { | ||
name: string; // name of the directive used in schema | ||
args: TArgs; // arguments provided to directive | ||
visitedType: VisitableSchemaType; // parent of visited object/field/... | ||
context: TContext; // current query context | ||
// (optional) If defined instance can visit field argument definitions | ||
visitIntrospectionArgument(result: GraphQLArgument, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLArgument> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `directive` definitions | ||
visitIntrospectionDirective(result: GraphQLDirective, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLDirective> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `enum` definitions | ||
visitIntrospectionEnum(result: GraphQLEnumType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLEnumType> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit enum value definitions | ||
visitIntrospectionEnumValue(result: GraphQLEnumValue, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLEnumValue> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit object field definitions | ||
visitIntrospectionField(result: GraphQLField<any, any>, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLField<any, any>> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit input field definitions | ||
visitIntrospectionInputField(result: GraphQLInputField, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInputField> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `input` object definitions | ||
visitIntrospectionInputObject(result: GraphQLInputObjectType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInputObjectType> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `interface` definitions | ||
visitIntrospectionInterface(result: GraphQLInterfaceType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInterfaceType> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit object `type` definitions | ||
visitIntrospectionObject(result: GraphQLObjectType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLObjectType> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `scalar` definitions | ||
visitIntrospectionScalar(result: GraphQLScalarType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLScalarType> { | ||
return this.authenticate(result); | ||
} | ||
// (optional) If defined instance can visit `union` definitions | ||
visitIntrospectionUnion(result: GraphQLUnionType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLUnionType> { | ||
return this.authenticate(result); | ||
} | ||
} | ||
export const mapper = { | ||
// (optional) If defined instance can visit `scalar` definitions | ||
[IntrospectionMapperKind.SCALAR_TYPE](result: GraphQLScalarType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit `enum` definitions | ||
[IntrospectionMapperKind.ENUM_TYPE](result: GraphQLEnumType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit object `type` definitions | ||
[IntrospectionMapperKind.OBJECT_TYPE](result: GraphQLObjectType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit `input` object definitions | ||
[IntrospectionMapperKind.INPUT_OBJECT_TYPE](result: GraphQLInputObjectType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit `union` definitions | ||
[IntrospectionMapperKind.UNION_TYPE](result: GraphQLUnionType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit `interface` definitions | ||
[IntrospectionMapperKind.INTERFACE_TYPE](result: GraphQLInterfaceType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit enum value definitions | ||
[IntrospectionMapperKind.ENUM_VALUE](result: GraphQLEnumValue, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit object field definitions | ||
[IntrospectionMapperKind.OBJECT_FIELD](result: GraphQLField<any, any>, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit field argument definitions | ||
[IntrospectionMapperKind.ARGUMENT](result: GraphQLArgument, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit input field definitions | ||
[IntrospectionMapperKind.INPUT_OBJECT_FIELD](result: GraphQLInputField, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
// (optional) If defined instance can visit `directive` definitions | ||
[IntrospectionMapperKind.DIRECTIVE](result: GraphQLDirective) { | ||
return wrap(result, schema, resolver); | ||
} | ||
} satisfies IntrospectionSchemaMapper & SchemaMapper; | ||
``` | ||
@@ -160,58 +148,102 @@ | ||
#### AuthenticationDirective | ||
#### Authentication mapper | ||
```ts | ||
class AuthenticationDirective extends SchemaDirectiveVisitor implements IntrospectionDirectiveVisitor { | ||
async authenticate(result: any) { | ||
if (!this.context.roles.includes(this.args.requires || 'ADMIN')) { | ||
return null; | ||
} | ||
return result; | ||
} | ||
visitIntrospectionArgument(result: GraphQLArgument, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLArgument> { | ||
return this.authenticate(result); | ||
} | ||
type WrappedResolverType<TSource, TContext, TArgs = any, TResult = unknown> = ( | ||
directive: Record<string, any>, orig: GraphQLFieldResolver<TSource, TContext, TArgs, TResult>, | ||
...passtrough: Parameters<GraphQLFieldResolver<TSource, TContext, TArgs>> | ||
) => TResult; | ||
visitIntrospectionDirective(result: GraphQLDirective, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLDirective> { | ||
return this.authenticate(result); | ||
} | ||
const check = (context: ContextType, requiredPermission?: string) => { | ||
// permission check | ||
}; | ||
visitIntrospectionEnum(result: GraphQLEnumType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLEnumType> { | ||
return this.authenticate(result); | ||
} | ||
const introspectionResolver = ( | ||
directive: Record<string, any>, orig: GraphQLFieldResolver<any, any>, | ||
parent: unknown, args: Record<string, unknown>, context: ContextType, info: GraphQLResolveInfo | ||
) => { | ||
if (check(context, directive.requires)) { | ||
return orig(parent, args, context, info); | ||
} | ||
visitIntrospectionEnumValue(result: GraphQLEnumValue, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLEnumValue> { | ||
return this.authenticate(result); | ||
} | ||
return null; | ||
}; | ||
visitIntrospectionField(result: GraphQLField<any, any>, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLField<any, any>> { | ||
return this.authenticate(result); | ||
} | ||
const resolver = ( | ||
directive: Record<string, any>, orig: GraphQLFieldResolver<any, any>, | ||
parent: unknown, args: Record<string, unknown>, context: ContextType, info: GraphQLResolveInfo | ||
) => { | ||
if (!check(context, directive.requires)) { | ||
throw new ValidationError(`Cannot query field "${info.fieldName}" on type "${info.parentType.name}".`); | ||
} | ||
visitIntrospectionInputField(result: GraphQLInputField, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInputField> { | ||
return this.authenticate(result); | ||
} | ||
return orig(parent, args, context, info); | ||
}; | ||
visitIntrospectionInputObject(result: GraphQLInputObjectType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInputObjectType> { | ||
return this.authenticate(result); | ||
} | ||
visitIntrospectionInterface(result: GraphQLInterfaceType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLInterfaceType> { | ||
return this.authenticate(result); | ||
} | ||
type WrappableIntrospectionType = VisitableIntrospectionType & { | ||
resolve: GraphQLFieldResolver<any, any> | null; | ||
subscribe: GraphQLFieldResolver<any, any> | null; // not precise | ||
} | ||
visitIntrospectionObject(result: GraphQLObjectType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLObjectType> { | ||
return this.authenticate(result); | ||
} | ||
const isWrappable = (val: any): val is WrappableIntrospectionType => !!val; | ||
visitIntrospectionScalar(result: GraphQLScalarType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLScalarType> { | ||
return this.authenticate(result); | ||
} | ||
const wrap = <T extends VisitableIntrospectionType>(result: T, schema: GraphQLSchema, resolver: WrappedResolverType<any, any>) => { | ||
const directive = getDirective(schema, result as any, 'auth')?.[0]; | ||
if (directive && isWrappable(result)) { | ||
const key = result.subscribe ? 'subscribe' : 'resolve'; | ||
const current = result[key]; | ||
visitIntrospectionUnion(result: GraphQLUnionType, info: GraphQLResolveInfo): IntrospectionVisitor<GraphQLUnionType> { | ||
return this.authenticate(result); | ||
result[key] = resolver.bind(undefined, directive, current || defaultFieldResolver); | ||
} | ||
return result; | ||
}; | ||
export const authMapper = { | ||
[IntrospectionMapperKind.SCALAR_TYPE](result: GraphQLScalarType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.ENUM_TYPE](result: GraphQLEnumType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.OBJECT_TYPE](result: GraphQLObjectType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.INPUT_OBJECT_TYPE](result: GraphQLInputObjectType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.UNION_TYPE](result: GraphQLUnionType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.INTERFACE_TYPE](result: GraphQLInterfaceType, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.ENUM_VALUE](result: GraphQLEnumValue, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.OBJECT_FIELD](result: GraphQLField<any, any>, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.ARGUMENT](result: GraphQLArgument, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.INPUT_OBJECT_FIELD](result: GraphQLInputField, parent: any, schema: GraphQLSchema) { | ||
return wrap(result, schema, introspectionResolver); | ||
}, | ||
[IntrospectionMapperKind.DIRECTIVE](result: GraphQLDirective) { | ||
if (result.name === 'auth' && isWrappable(result)) { | ||
result.resolve = () => null; | ||
} | ||
// .... | ||
} | ||
return result; | ||
}, | ||
[MapperKind.ENUM_TYPE](result, schema) { | ||
return wrap(result, schema, resolver); | ||
}, | ||
[MapperKind.OBJECT_FIELD](result, _, __, schema) { | ||
return wrap(result as any, schema, resolver); | ||
}, | ||
[MapperKind.INPUT_OBJECT_FIELD](result, _, __, schema) { | ||
return wrap(result as any, schema, resolver); | ||
} | ||
} satisfies IntrospectionSchemaMapper & SchemaMapper; | ||
``` | ||
@@ -221,12 +253,8 @@ | ||
```ts | ||
import makeExecutableSchema from 'graphql-introspection-filtering'; | ||
import makeExecutableSchema, { mapSchema } from 'graphql-introspection-filtering'; | ||
const schema = makeExecutableSchema({ | ||
export default mapSchema(makeExecutableSchema({ | ||
typeDefs: ...schema..., | ||
..., | ||
schemaDirectives: { | ||
auth: AuthenticationDirective | ||
}, | ||
shouldSkipQuery: 1 // skip initial query, which is executeted to hash schema | ||
}); | ||
}), authMapper); | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
66016
2.82%1
-50%257
12.23%921
-0.11%