Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion packages/graphql/lib/decorators/args-type.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import { ClassType } from '../enums/class-type.enum';
import { RegisterInOption } from '../schema-builder/metadata';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util';

/**
* Interface defining options that can be passed to `@ArgsType()` decorator.
*
* @publicApi
*/
export interface ArgsTypeOptions {
/**
* NestJS module that this type belongs to.
* When specified, this type will only be included in GraphQL schemas
* that include this module via the `include` option.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}

/**
* Decorator that marks a class as a resolver arguments type.
*
* @publicApi
*/
export function ArgsType(): ClassDecorator;
/**
* Decorator that marks a class as a resolver arguments type.
*
* @publicApi
*/
export function ArgsType(options: ArgsTypeOptions): ClassDecorator;
/**
* Decorator that marks a class as a resolver arguments type.
*
* @publicApi
*/
export function ArgsType(): ClassDecorator {
export function ArgsType(options?: ArgsTypeOptions): ClassDecorator {
return (target: Function) => {
const metadata = {
name: target.name,
target,
registerIn: options?.registerIn,
};
LazyMetadataStorage.store(() =>
TypeMetadataStorage.addArgsMetadata(metadata),
Expand Down
9 changes: 9 additions & 0 deletions packages/graphql/lib/decorators/input-type.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { isString } from '@nestjs/common/utils/shared.utils';
import { ClassType } from '../enums/class-type.enum';
import { RegisterInOption } from '../schema-builder/metadata';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util';
Expand All @@ -30,6 +31,13 @@ export interface InputTypeOptions {
* More info about '@oneOf' types in the [GraphQL spec](https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects).
*/
isOneOf?: boolean;
/**
* NestJS module that this type belongs to.
* When specified, this type will only be included in GraphQL schemas
* that include this module via the `include` option.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}

/**
Expand Down Expand Up @@ -73,6 +81,7 @@ export function InputType(
description: options.description,
isAbstract: options.isAbstract,
isOneOf: options.isOneOf,
registerIn: options.registerIn,
};
LazyMetadataStorage.store(() =>
TypeMetadataStorage.addInputTypeMetadata(metadata),
Expand Down
9 changes: 9 additions & 0 deletions packages/graphql/lib/decorators/interface-type.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { isString } from '@nestjs/common/utils/shared.utils';
import { ClassType } from '../enums/class-type.enum';
import { ResolveTypeFn } from '../interfaces';
import { RegisterInOption } from '../schema-builder/metadata';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util';
Expand All @@ -34,6 +35,13 @@ export interface InterfaceTypeOptions {
* Interfaces implemented by this interface.
*/
implements?: Function | Function[] | (() => Function | Function[]);
/**
* NestJS module that this type belongs to.
* When specified, this type will only be included in GraphQL schemas
* that include this module via the `include` option.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}

/**
Expand Down Expand Up @@ -71,6 +79,7 @@ export function InterfaceType(
target,
...options,
interfaces: options.implements,
registerIn: options.registerIn,
};
TypeMetadataStorage.addInterfaceMetadata(metadata);
};
Expand Down
9 changes: 9 additions & 0 deletions packages/graphql/lib/decorators/object-type.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { isString } from '@nestjs/common/utils/shared.utils';
import { ClassType } from '../enums/class-type.enum';
import { RegisterInOption } from '../schema-builder/metadata';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { addClassTypeMetadata } from '../utils/add-class-type-metadata.util';
Expand Down Expand Up @@ -34,6 +35,13 @@ export interface ObjectTypeOptions {
* Also works on classes marked with `isAbstract: true`.
*/
inheritDescription?: boolean;
/**
* NestJS module that this type belongs to.
* When specified, this type will only be included in GraphQL schemas
* that include this module via the `include` option.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}

/**
Expand Down Expand Up @@ -85,6 +93,7 @@ export function ObjectType(
interfaces: options.implements,
isAbstract: options.isAbstract,
inheritDescription: options.inheritDescription,
registerIn: options.registerIn,
});

// This function must be called eagerly to allow resolvers
Expand Down
5 changes: 4 additions & 1 deletion packages/graphql/lib/graphql-schema.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export class GraphQLSchemaBuilder {
) ?? true,
),
],
},
// Pass include modules for type filtering in Code First approach
// This property is handled internally by GraphQLSchemaFactory
includeModules: options.include,
} as BuildSchemaOptions & { includeModules?: Function[] },
options.sortSchema,
options.transformAutoSchemaFile && options.transformSchema,
);
Expand Down
16 changes: 15 additions & 1 deletion packages/graphql/lib/schema-builder/graphql-schema.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ import { LazyMetadataStorage } from './storages/lazy-metadata.storage';
import { TypeMetadataStorage } from './storages/type-metadata.storage';
import { TypeDefinitionsGenerator } from './type-definitions.generator';

/**
* Internal options interface that extends BuildSchemaOptions with includeModules.
* This is used internally by GraphQLSchemaBuilder to pass module filtering options.
* @internal
*/
interface InternalBuildSchemaOptions extends BuildSchemaOptions {
includeModules?: Function[];
}

@Injectable()
export class GraphQLSchemaFactory {
private readonly logger = new Logger(GraphQLSchemaFactory.name);
Expand Down Expand Up @@ -63,7 +72,12 @@ export class GraphQLSchemaFactory {
LazyMetadataStorage.load(resolvers);
TypeMetadataStorage.compile(options.orphanedTypes);

this.typeDefinitionsGenerator.generate(options);
// includeModules is passed internally from GraphQLSchemaBuilder
const internalOptions = options as InternalBuildSchemaOptions;
this.typeDefinitionsGenerator.generate(
options,
internalOptions.includeModules,
);

const schema = new GraphQLSchema({
mutation: this.mutationTypeFactory.create(resolvers, options),
Expand Down
14 changes: 14 additions & 0 deletions packages/graphql/lib/schema-builder/metadata/class.metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { DirectiveMetadata } from './directive.metadata';
import { PropertyMetadata } from './property.metadata';

/**
* Type for the registerIn option used in GraphQL type decorators.
* Can be a module class directly or a function that returns the module class
* (useful to avoid circular dependency issues).
*
* @publicApi
*/
export type RegisterInOption = Function | (() => Function);

export interface ClassMetadata {
target: Function;
name: string;
Expand All @@ -11,4 +20,9 @@ export interface ClassMetadata {
properties?: PropertyMetadata[];
inheritDescription?: boolean;
isOneOf?: boolean; // For '@oneOf' input types
/**
* NestJS module that this type belongs to.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}
7 changes: 7 additions & 0 deletions packages/graphql/lib/schema-builder/metadata/enum.metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RegisterInOption } from './class.metadata';

export interface EnumMetadataValuesMapOptions {
deprecationReason?: string;
description?: string;
Expand All @@ -12,4 +14,9 @@ export interface EnumMetadata<T extends object = any> {
name: string;
description?: string;
valuesMap?: EnumMetadataValuesMap<T>;
/**
* NestJS module that this enum belongs to.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type } from '@nestjs/common';
import { ResolveTypeFn } from '../../interfaces';
import { RegisterInOption } from './class.metadata';

export interface UnionMetadata<
T extends readonly Type<unknown>[] = readonly Type<unknown>[],
Expand All @@ -9,4 +10,9 @@ export interface UnionMetadata<
id?: symbol;
description?: string;
resolveType?: ResolveTypeFn;
/**
* NestJS module that this union belongs to.
* @see RegisterInOption for details
*/
registerIn?: RegisterInOption;
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,18 @@ export class TypeDefinitionsStorage {
}
return;
}

/**
* Clear all type definitions.
* Used when generating multiple schemas with different module configurations.
*/
clear() {
this.interfaceTypeDefinitions.clear();
this.enumTypeDefinitions.clear();
this.unionTypeDefinitions.clear();
this.objectTypeDefinitions.clear();
this.inputTypeDefinitions.clear();
this.inputTypeDefinitionsLinks = undefined;
this.outputTypeDefinitionsLinks = undefined;
}
}
115 changes: 114 additions & 1 deletion packages/graphql/lib/schema-builder/storages/type-metadata.storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type } from '@nestjs/common';
import { Logger, Type } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import { addFieldMetadata } from '../../decorators';
import { METADATA_FACTORY_NAME } from '../../plugin/plugin-constants';
Expand All @@ -16,6 +16,7 @@ import {
PropertyDirectiveMetadata,
PropertyExtensionsMetadata,
PropertyMetadata,
RegisterInOption,
ResolverClassMetadata,
ResolverTypeMetadata,
UnionMetadata,
Expand All @@ -25,6 +26,7 @@ import { ObjectTypeMetadata } from '../metadata/object-type.metadata';
import { isThrowing } from '../utils/is-throwing.util';

export class TypeMetadataStorageHost {
private readonly logger = new Logger(TypeMetadataStorageHost.name);
private queries = new Array<ResolverTypeMetadata>();
private mutations = new Array<ResolverTypeMetadata>();
private subscriptions = new Array<ResolverTypeMetadata>();
Expand Down Expand Up @@ -140,6 +142,117 @@ export class TypeMetadataStorageHost {
return this.unions;
}

/**
* Resolves the registerIn value, which can be either a module class directly
* or a function that returns the module class (for circular dependency handling).
*/
private resolveRegisterIn(
registerIn: RegisterInOption | undefined,
): Function | undefined {
if (!registerIn) {
return undefined;
}
// Check if it's a class (has prototype with constructor)
// or an arrow function / factory function (no prototype)
if (
registerIn.prototype &&
registerIn.prototype.constructor === registerIn
) {
// It's a class, return directly
return registerIn;
}
// It's a factory function, call it to get the actual module
try {
return registerIn();
} catch (error) {
this.logger.warn(
`Failed to resolve registerIn function. The type will be included in all schemas (backward compatible behavior). Error: ${error}`,
);
return undefined;
}
}

/**
* Checks if the metadata should be included based on module filtering.
*/
private shouldIncludeInModules(
registerIn: RegisterInOption | undefined,
moduleSet: Set<Function>,
): boolean {
const resolvedModule = this.resolveRegisterIn(registerIn);
// Include if no registerIn specified (included in all schemas) or if in the module set
return !resolvedModule || moduleSet.has(resolvedModule);
}

/**
* Generic helper to filter metadata by modules.
* Returns items that either have no registerIn specified (included in all schemas)
* or are registered in one of the specified modules.
*/
private filterByModules<T extends { registerIn?: RegisterInOption }>(
metadata: T[],
modules: Function[],
): T[] {
const moduleSet = new Set(modules);
return metadata.filter((item) =>
this.shouldIncludeInModules(item.registerIn, moduleSet),
);
}

/**
* Get ObjectType metadata filtered by modules.
*/
getObjectTypesMetadataByModules(modules: Function[]): ObjectTypeMetadata[] {
return this.filterByModules(
this.metadataByTargetCollection.all.objectType,
modules,
);
}

/**
* Get InputType metadata filtered by modules.
*/
getInputTypesMetadataByModules(modules: Function[]): ClassMetadata[] {
return this.filterByModules(
this.metadataByTargetCollection.all.inputType,
modules,
);
}

/**
* Get InterfaceType metadata filtered by modules.
*/
getInterfacesMetadataByModules(modules: Function[]): InterfaceMetadata[] {
return this.filterByModules(
[...this.metadataByTargetCollection.all.interface.values()],
modules,
);
}

/**
* Get ArgsType metadata filtered by modules.
*/
getArgumentsMetadataByModules(modules: Function[]): ClassMetadata[] {
return this.filterByModules(
this.metadataByTargetCollection.all.argumentType,
modules,
);
}

/**
* Get Enum metadata filtered by modules.
*/
getEnumsMetadataByModules(modules: Function[]): EnumMetadata[] {
return this.filterByModules(this.enums, modules);
}

/**
* Get Union metadata filtered by modules.
*/
getUnionsMetadataByModules(modules: Function[]): UnionMetadata[] {
return this.filterByModules(this.unions, modules);
}

addDirectiveMetadata(metadata: ClassDirectiveMetadata) {
const classMetadata = this.metadataByTargetCollection.get(metadata.target);
if (!classMetadata.fieldDirectives.sdls.has(metadata.sdl)) {
Expand Down
Loading