Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/seven-hounds-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@envelop/generic-auth': patch
---

Remove empty and unused nodes from redacted query after validation
7 changes: 7 additions & 0 deletions .changeset/young-beans-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@envelop/extended-validation': minor
---

Introduce new option `onDocument`

It's a callback that is invoked when the document is assembled by the visitor.
16 changes: 12 additions & 4 deletions packages/plugins/extended-validation/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DocumentNode,
ExecutionArgs,
ExecutionResult,
GraphQLError,
Expand Down Expand Up @@ -34,6 +35,10 @@ type OnValidationFailedCallback = (params: {

export const useExtendedValidation = <PluginContext extends Record<string, any> = {}>(options: {
rules: Array<ExtendedValidationRule>;
/**
* Callback that is invoked when the document is assembled by the visitor.
*/
onDocument?: (document: DocumentNode) => DocumentNode;
/**
* Callback that is invoked if the extended validation yields any errors.
*/
Expand Down Expand Up @@ -74,12 +79,14 @@ export const useExtendedValidation = <PluginContext extends Record<string, any>
onSubscribe: buildHandler(
'subscribe',
getTypeInfo,
options.onDocument,
options.onValidationFailed,
options.rejectOnErrors !== false,
),
onExecute: buildHandler(
'execute',
getTypeInfo,
options.onDocument,
options.onValidationFailed,
options.rejectOnErrors !== false,
),
Expand All @@ -89,6 +96,7 @@ export const useExtendedValidation = <PluginContext extends Record<string, any>
function buildHandler(
name: 'execute' | 'subscribe',
getTypeInfo: () => TypeInfo | undefined,
onDocument?: (document: DocumentNode) => DocumentNode,
onValidationFailed?: OnValidationFailedCallback,
rejectOnErrors = true,
) {
Expand Down Expand Up @@ -127,6 +135,9 @@ function buildHandler(
);

args.document = visit(args.document, visitWithTypeInfo(typeInfo, visitor));
if (onDocument) {
args.document = onDocument(args.document);
}

if (errors.length > 0) {
if (rejectOnErrors) {
Expand Down Expand Up @@ -164,10 +175,7 @@ function buildHandler(
for (const pathItemIndex in path.slice(0, -1)) {
const pathItem = path[pathItemIndex];
currentData = currentData[pathItem] ||=
typeof path[Number(pathItemIndex) + 1] === 'number' ||
path[Number(pathItemIndex) + 1]
? []
: {};
typeof path[Number(pathItemIndex) + 1] === 'number' ? [] : {};
if (Array.isArray(currentData)) {
let pathItemIndexInArray = Number(pathItemIndex) + 1;
if (path[pathItemIndexInArray] === '@') {
Expand Down
90 changes: 58 additions & 32 deletions packages/plugins/generic-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
GraphQLField,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLOutputType,
isAbstractType,
isInterfaceType,
isIntrospectionType,
Expand All @@ -27,6 +26,7 @@ import {
shouldIncludeNode,
} from '@graphql-tools/utils';
import { handleMaybePromise } from '@whatwg-node/promise-helpers';
import { removeEmptyOrUnusedNodes } from './utils.js';

export type ResolveUserFn<UserType, ContextType = DefaultContext> = (
context: ContextType,
Expand Down Expand Up @@ -316,6 +316,7 @@ export const useGenericAuth = <
addPlugin(
useExtendedValidation({
rejectOnErrors: rejectUnauthenticated,
onDocument: removeEmptyOrUnusedNodes,
rules: [
function AuthorizationExtendedValidationRule(context, args) {
const user = (args.contextValue as any)[contextFieldName];
Expand All @@ -336,6 +337,48 @@ export const useGenericAuth = <
}
const operationType = operationAST?.operation ?? ('query' as OperationTypeNode);

const fragmentPaths = new Map<string, ReadonlyArray<string | number>>();

function getResolvePath(
path: readonly (string | number)[],
currType: GraphQLObjectType,
): (string | number)[] {
const resolvePath: (string | number)[] = [];
let curr: any = args.document;
for (const pathItem of path) {
curr = curr[pathItem];
if (curr?.kind === 'Field') {
const fieldName = curr.name.value;
const responseKey = curr.alias?.value ?? fieldName;
let field: GraphQLField<any, any> | undefined;
if (isObjectType(currType)) {
field = currType.getFields()[fieldName];
} else if (isAbstractType(currType)) {
for (const possibleType of schema.getPossibleTypes(currType)) {
field = possibleType.getFields()[fieldName];
if (field) {
break;
}
}
}
if (isListType(field?.type)) {
resolvePath.push('@');
}
resolvePath.push(responseKey);
if (field?.type) {
currType = getNamedType(field.type) as GraphQLObjectType;
}
} else if (curr?.kind === 'FragmentDefinition') {
currType = schema.getType(curr.typeCondition.name.value) as GraphQLObjectType;
const fragmentPath = fragmentPaths.get(curr.name.value);
if (fragmentPath) {
resolvePath.push(...fragmentPath);
}
}
}
return resolvePath;
}

const handleField = (
{
node: fieldNode,
Expand Down Expand Up @@ -367,38 +410,10 @@ export const useGenericAuth = <
const userPolicies =
policiesByContext.get(args.contextValue as unknown as ContextType) ?? [];

const resolvePath: (string | number)[] = [];

let curr: any = args.document;
let currType: GraphQLOutputType | undefined | null = getDefinedRootType(
schema,
operationType,
const resolvePath: (string | number)[] = getResolvePath(
path,
getDefinedRootType(schema, operationType),
);
for (const pathItem of path) {
curr = curr[pathItem];
if (curr?.kind === 'Field') {
const fieldName = curr.name.value;
const responseKey = curr.alias?.value ?? fieldName;
let field: GraphQLField<any, any> | undefined;
if (isObjectType(currType)) {
field = currType.getFields()[fieldName];
} else if (isAbstractType(currType)) {
for (const possibleType of schema.getPossibleTypes(currType)) {
field = possibleType.getFields()[fieldName];
if (field) {
break;
}
}
}
if (isListType(field?.type)) {
resolvePath.push('@');
}
resolvePath.push(responseKey);
if (field?.type) {
currType = getNamedType(field.type);
}
}
}

return validateUser({
user,
Expand All @@ -421,6 +436,17 @@ export const useGenericAuth = <
};

return {
FragmentSpread(node, key, parent, path, ancestors) {
const fragmentName = node.name.value;
const fragment = context.getFragment(fragmentName);
if (fragment) {
const resolvePath = getResolvePath(
path,
schema.getType(fragment.typeCondition.name.value) as GraphQLObjectType,
);
fragmentPaths.set(fragmentName, resolvePath);
}
},
Field(node, key, parent, path, ancestors) {
if (variableValues && !shouldIncludeNode(variableValues, node)) {
return;
Expand Down
Loading