Skip to content

Commit

Permalink
feat(validation): inject resolver data to custom validate fn
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Jul 5, 2023
1 parent cb9b012 commit 2133c81
Show file tree
Hide file tree
Showing 10 changed files with 34 additions and 16 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
### Features
- **Breaking Change**: upgrade `ArgumentValidationError` and replace `UnauthorizedError` and `ForbiddenError` with `AuthenticationError`, `AuthorizationError` that are extending `GraphQLError` to let the error details be accessible in the `extensions` property
- **Breaking Change**: change `ClassType` constraint from `ClassType<T = any>` to `ClassType<T extends object = object>` in order to make it work properly with new TS features
- **Breaking Change**: removed `dateScalarMode` option from `buildSchema`
- **Breaking Change**: remove `dateScalarMode` option from `buildSchema`
- **Breaking Change**: make `graphql-scalars` package a peer dependency and use date scalars from it instead of custom ones
- **Breaking Change**: exported `GraphQLISODateTime` scalar has now a name `DateTimeISO`
- **Breaking Change**: change `ValidatorFn` signature from `ValidatorFn<TArgs>` to `ValidatorFn<TContext>`
- support custom validation function getting resolver data on validate

### Fixes
- allow `ValidatorFn` to accept array of values (instead of only `object | undefined`)
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/createMethodDecorator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UseMiddleware } from "./UseMiddleware";
import { MiddlewareFn } from "../interfaces/Middleware";

export function createMethodDecorator<TContextType = {}>(
export function createMethodDecorator<TContextType extends object = object>(
resolver: MiddlewareFn<TContextType>,
): MethodDecorator {
return UseMiddleware(resolver);
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/createParamDecorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { SymbolKeysNotSupportedError } from "../errors";
import { ParameterDecorator } from "../interfaces/LegacyDecorators";

export function createParamDecorator<TContextType = {}>(
export function createParamDecorator<TContextType extends object = object>(
resolver: (resolverData: ResolverData<TContextType>) => any,
): ParameterDecorator {
return (prototype, propertyKey, parameterIndex) => {
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/AuthChecker.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { ClassType } from "./ClassType";
import { ResolverData } from "./ResolverData";

export type AuthCheckerFn<TContextType = {}, TRoleType = string> = (
export type AuthCheckerFn<TContextType extends object = object, TRoleType = string> = (
resolverData: ResolverData<TContextType>,
roles: TRoleType[],
) => boolean | Promise<boolean>;

export type AuthCheckerInterface<TContextType = {}, TRoleType = string> = {
export type AuthCheckerInterface<TContextType extends object = object, TRoleType = string> = {
check(resolverData: ResolverData<TContextType>, roles: TRoleType[]): boolean | Promise<boolean>;
};

export type AuthChecker<TContextType = {}, TRoleType = string> =
export type AuthChecker<TContextType extends object = object, TRoleType = string> =
| AuthCheckerFn<TContextType, TRoleType>
| ClassType<AuthCheckerInterface<TContextType, TRoleType>>;

Expand Down
10 changes: 6 additions & 4 deletions src/interfaces/Middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { ResolverData } from "./ResolverData";

export type NextFn = () => Promise<any>;

export type MiddlewareFn<TContext = {}> = (
export type MiddlewareFn<TContext extends object = object> = (
action: ResolverData<TContext>,
next: NextFn,
) => Promise<any>;

export interface MiddlewareInterface<TContext = {}> {
export interface MiddlewareInterface<TContext extends object = object> {
use: MiddlewareFn<TContext>;
}
export interface MiddlewareClass<TContext = {}> {
export interface MiddlewareClass<TContext extends object = object> {
new (...args: any[]): MiddlewareInterface<TContext>;
}

export type Middleware<TContext = {}> = MiddlewareFn<TContext> | MiddlewareClass<TContext>;
export type Middleware<TContext extends object = object> =
| MiddlewareFn<TContext>
| MiddlewareClass<TContext>;
4 changes: 2 additions & 2 deletions src/interfaces/ResolverData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ export interface ArgsDictionary {
[argName: string]: any;
}

export interface ResolverData<ContextType = {}> {
export interface ResolverData<TContextType extends object = object> {
root: any;
args: ArgsDictionary;
context: ContextType;
context: TContextType;
info: GraphQLResolveInfo;
}
4 changes: 3 additions & 1 deletion src/interfaces/ValidatorFn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TypeValue } from "../decorators/types";
import { ResolverData } from "./ResolverData";

export type ValidatorFn = (
export type ValidatorFn<TContext extends object = object> = (
/**
* The value of the argument.
* It can by of any type, which means:
Expand All @@ -12,4 +13,5 @@ export type ValidatorFn = (
*/
argValue: any | undefined,
argType: TypeValue,
resolverData: ResolverData<TContext>,
) => void | Promise<void>;
2 changes: 2 additions & 0 deletions src/resolvers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function getParams(
return validateArg(
convertArgsToInstance(paramInfo, resolverData.args),
paramInfo.getType(),
resolverData,
globalValidate,
paramInfo.validate,
validateFn,
Expand All @@ -35,6 +36,7 @@ export function getParams(
return validateArg(
convertArgToInstance(paramInfo, resolverData.args),
paramInfo.getType(),
resolverData,
globalValidate,
paramInfo.validate,
validateFn,
Expand Down
4 changes: 3 additions & 1 deletion src/resolvers/validate-arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import { TypeValue } from "../decorators/types";
import { ArgumentValidationError } from "../errors";
import { ValidateSettings } from "../schema/build-context";
import { ValidatorFn } from "../interfaces/ValidatorFn";
import { ResolverData } from "../interfaces";

const shouldArgBeValidated = (argValue: unknown): boolean =>
argValue !== null && typeof argValue === "object";

export async function validateArg(
argValue: any | undefined,
argType: TypeValue,
resolverData: ResolverData,
globalValidate: ValidateSettings,
argValidate: ValidateSettings | undefined,
validateFn: ValidatorFn | undefined,
): Promise<any | undefined> {
if (typeof validateFn === "function") {
await validateFn(argValue, argType);
await validateFn(argValue, argType, resolverData);
return argValue;
}

Expand Down
12 changes: 10 additions & 2 deletions tests/functional/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ArgumentValidationError,
Args,
ArgsType,
ResolverData,
} from "../../src";
import { TypeValue } from "../../src/decorators/types";

Expand Down Expand Up @@ -678,6 +679,7 @@ describe("Custom validation", () => {

let validateArgs: Array<any | undefined> = [];
let validateTypes: TypeValue[] = [];
let validateResolverData: ResolverData[] = [];
let sampleQueryArgs: any[] = [];

beforeAll(async () => {
Expand Down Expand Up @@ -721,17 +723,23 @@ describe("Custom validation", () => {
it("should call `validateFn` function provided in option with proper params", async () => {
schema = await buildSchema({
resolvers: [sampleResolverCls],
validateFn: (arg, type) => {
validateFn: (arg, type, resolverData) => {
validateArgs.push(arg);
validateTypes.push(type);
validateResolverData.push(resolverData);
},
});

await graphql({ schema, source: document });
await graphql({ schema, source: document, contextValue: { isContext: true } });

expect(validateArgs).toEqual([{ sampleField: "sampleFieldValue" }]);
expect(validateArgs[0]).toBeInstanceOf(sampleArgsCls);
expect(validateTypes).toEqual([sampleArgsCls]);
expect(validateResolverData).toEqual([
expect.objectContaining({
context: { isContext: true },
}),
]);
});

it("should let `validateFn` function handle array args", async () => {
Expand Down

0 comments on commit 2133c81

Please sign in to comment.