diff --git a/packages/graphql/lib/decorators/args-type.decorator.ts b/packages/graphql/lib/decorators/args-type.decorator.ts index 423bb8c71..8e18beba2 100644 --- a/packages/graphql/lib/decorators/args-type.decorator.ts +++ b/packages/graphql/lib/decorators/args-type.decorator.ts @@ -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), diff --git a/packages/graphql/lib/decorators/input-type.decorator.ts b/packages/graphql/lib/decorators/input-type.decorator.ts index e37864bd3..ab8f48534 100644 --- a/packages/graphql/lib/decorators/input-type.decorator.ts +++ b/packages/graphql/lib/decorators/input-type.decorator.ts @@ -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'; @@ -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; } /** @@ -73,6 +81,7 @@ export function InputType( description: options.description, isAbstract: options.isAbstract, isOneOf: options.isOneOf, + registerIn: options.registerIn, }; LazyMetadataStorage.store(() => TypeMetadataStorage.addInputTypeMetadata(metadata), diff --git a/packages/graphql/lib/decorators/interface-type.decorator.ts b/packages/graphql/lib/decorators/interface-type.decorator.ts index 641ffdf56..d7bc4552a 100644 --- a/packages/graphql/lib/decorators/interface-type.decorator.ts +++ b/packages/graphql/lib/decorators/interface-type.decorator.ts @@ -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'; @@ -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; } /** @@ -71,6 +79,7 @@ export function InterfaceType( target, ...options, interfaces: options.implements, + registerIn: options.registerIn, }; TypeMetadataStorage.addInterfaceMetadata(metadata); }; diff --git a/packages/graphql/lib/decorators/object-type.decorator.ts b/packages/graphql/lib/decorators/object-type.decorator.ts index 0b015ff4b..83b68ceb6 100644 --- a/packages/graphql/lib/decorators/object-type.decorator.ts +++ b/packages/graphql/lib/decorators/object-type.decorator.ts @@ -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'; @@ -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; } /** @@ -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 diff --git a/packages/graphql/lib/graphql-schema.builder.ts b/packages/graphql/lib/graphql-schema.builder.ts index 5fe86230d..db9daa69d 100644 --- a/packages/graphql/lib/graphql-schema.builder.ts +++ b/packages/graphql/lib/graphql-schema.builder.ts @@ -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, ); diff --git a/packages/graphql/lib/schema-builder/graphql-schema.factory.ts b/packages/graphql/lib/schema-builder/graphql-schema.factory.ts index 55db1d34e..d5f1d5da1 100644 --- a/packages/graphql/lib/schema-builder/graphql-schema.factory.ts +++ b/packages/graphql/lib/schema-builder/graphql-schema.factory.ts @@ -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); @@ -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), diff --git a/packages/graphql/lib/schema-builder/metadata/class.metadata.ts b/packages/graphql/lib/schema-builder/metadata/class.metadata.ts index 4ff8f3d7d..ed961e4d7 100644 --- a/packages/graphql/lib/schema-builder/metadata/class.metadata.ts +++ b/packages/graphql/lib/schema-builder/metadata/class.metadata.ts @@ -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; @@ -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; } diff --git a/packages/graphql/lib/schema-builder/metadata/enum.metadata.ts b/packages/graphql/lib/schema-builder/metadata/enum.metadata.ts index ea05f820c..b26a9f247 100644 --- a/packages/graphql/lib/schema-builder/metadata/enum.metadata.ts +++ b/packages/graphql/lib/schema-builder/metadata/enum.metadata.ts @@ -1,3 +1,5 @@ +import { RegisterInOption } from './class.metadata'; + export interface EnumMetadataValuesMapOptions { deprecationReason?: string; description?: string; @@ -12,4 +14,9 @@ export interface EnumMetadata { name: string; description?: string; valuesMap?: EnumMetadataValuesMap; + /** + * NestJS module that this enum belongs to. + * @see RegisterInOption for details + */ + registerIn?: RegisterInOption; } diff --git a/packages/graphql/lib/schema-builder/metadata/union.metadata.ts b/packages/graphql/lib/schema-builder/metadata/union.metadata.ts index 4211481d9..d93be37b9 100644 --- a/packages/graphql/lib/schema-builder/metadata/union.metadata.ts +++ b/packages/graphql/lib/schema-builder/metadata/union.metadata.ts @@ -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[] = readonly Type[], @@ -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; } diff --git a/packages/graphql/lib/schema-builder/storages/type-definitions.storage.ts b/packages/graphql/lib/schema-builder/storages/type-definitions.storage.ts index f063327bc..71da19752 100644 --- a/packages/graphql/lib/schema-builder/storages/type-definitions.storage.ts +++ b/packages/graphql/lib/schema-builder/storages/type-definitions.storage.ts @@ -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; + } } diff --git a/packages/graphql/lib/schema-builder/storages/type-metadata.storage.ts b/packages/graphql/lib/schema-builder/storages/type-metadata.storage.ts index 4af81b1d3..59fc96299 100644 --- a/packages/graphql/lib/schema-builder/storages/type-metadata.storage.ts +++ b/packages/graphql/lib/schema-builder/storages/type-metadata.storage.ts @@ -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'; @@ -16,6 +16,7 @@ import { PropertyDirectiveMetadata, PropertyExtensionsMetadata, PropertyMetadata, + RegisterInOption, ResolverClassMetadata, ResolverTypeMetadata, UnionMetadata, @@ -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(); private mutations = new Array(); private subscriptions = new Array(); @@ -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, + ): 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( + 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)) { diff --git a/packages/graphql/lib/schema-builder/type-definitions.generator.ts b/packages/graphql/lib/schema-builder/type-definitions.generator.ts index a60b321e7..1f6c67272 100644 --- a/packages/graphql/lib/schema-builder/type-definitions.generator.ts +++ b/packages/graphql/lib/schema-builder/type-definitions.generator.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; import { BuildSchemaOptions } from '../interfaces'; +import { ClassMetadata, EnumMetadata, UnionMetadata } from './metadata'; +import { InterfaceMetadata } from './metadata/interface.metadata'; +import { ObjectTypeMetadata } from './metadata/object-type.metadata'; import { EnumDefinitionFactory } from './factories/enum-definition.factory'; import { InputTypeDefinitionFactory } from './factories/input-type-definition.factory'; import { InterfaceDefinitionFactory } from './factories/interface-definition.factory'; @@ -19,50 +22,88 @@ export class TypeDefinitionsGenerator { private readonly unionDefinitionFactory: UnionDefinitionFactory, ) {} - generate(options: BuildSchemaOptions) { - this.generateUnionDefs(); - this.generateEnumDefs(); - this.generateInterfaceDefs(options); - this.generateObjectTypeDefs(options); - this.generateInputTypeDefs(options); + generate(options: BuildSchemaOptions, includeModules?: Function[]) { + // Clear previous type definitions to support multiple schema generation + this.typeDefinitionsStorage.clear(); + + if (includeModules?.length) { + // Filter metadata by modules + this.generateUnionDefs( + TypeMetadataStorage.getUnionsMetadataByModules(includeModules), + ); + this.generateEnumDefs( + TypeMetadataStorage.getEnumsMetadataByModules(includeModules), + ); + this.generateInterfaceDefs( + options, + TypeMetadataStorage.getInterfacesMetadataByModules(includeModules), + ); + this.generateObjectTypeDefs( + options, + TypeMetadataStorage.getObjectTypesMetadataByModules(includeModules), + ); + this.generateInputTypeDefs( + options, + TypeMetadataStorage.getInputTypesMetadataByModules(includeModules), + ); + } else { + // Use all metadata when no module filter is specified + this.generateUnionDefs(); + this.generateEnumDefs(); + this.generateInterfaceDefs(options); + this.generateObjectTypeDefs(options); + this.generateInputTypeDefs(options); + } } - private generateInputTypeDefs(options: BuildSchemaOptions) { - const metadata = TypeMetadataStorage.getInputTypesMetadata(); - const inputTypeDefs = metadata.map((metadata) => - this.inputTypeDefinitionFactory.create(metadata, options), + private generateInputTypeDefs( + options: BuildSchemaOptions, + metadata?: ClassMetadata[], + ) { + const inputTypeMetadata = + metadata ?? TypeMetadataStorage.getInputTypesMetadata(); + const inputTypeDefs = inputTypeMetadata.map((item) => + this.inputTypeDefinitionFactory.create(item, options), ); this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); } - private generateObjectTypeDefs(options: BuildSchemaOptions) { - const metadata = TypeMetadataStorage.getObjectTypesMetadata(); - const objectTypeDefs = metadata.map((metadata) => - this.objectTypeDefinitionFactory.create(metadata, options), + private generateObjectTypeDefs( + options: BuildSchemaOptions, + metadata?: ObjectTypeMetadata[], + ) { + const objectTypeMetadata = + metadata ?? TypeMetadataStorage.getObjectTypesMetadata(); + const objectTypeDefs = objectTypeMetadata.map((item) => + this.objectTypeDefinitionFactory.create(item, options), ); this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); } - private generateInterfaceDefs(options: BuildSchemaOptions) { - const metadata = TypeMetadataStorage.getInterfacesMetadata(); - const interfaceDefs = metadata.map((metadata) => - this.interfaceDefinitionFactory.create(metadata, options), + private generateInterfaceDefs( + options: BuildSchemaOptions, + metadata?: InterfaceMetadata[], + ) { + const interfaceMetadata = + metadata ?? TypeMetadataStorage.getInterfacesMetadata(); + const interfaceDefs = interfaceMetadata.map((item) => + this.interfaceDefinitionFactory.create(item, options), ); this.typeDefinitionsStorage.addInterfaces(interfaceDefs); } - private generateEnumDefs() { - const metadata = TypeMetadataStorage.getEnumsMetadata(); - const enumDefs = metadata.map((metadata) => - this.enumDefinitionFactory.create(metadata), + private generateEnumDefs(metadata?: EnumMetadata[]) { + const enumMetadata = metadata ?? TypeMetadataStorage.getEnumsMetadata(); + const enumDefs = enumMetadata.map((item) => + this.enumDefinitionFactory.create(item), ); this.typeDefinitionsStorage.addEnums(enumDefs); } - private generateUnionDefs() { - const metadata = TypeMetadataStorage.getUnionsMetadata(); - const unionDefs = metadata.map((metadata) => - this.unionDefinitionFactory.create(metadata), + private generateUnionDefs(metadata?: UnionMetadata[]) { + const unionMetadata = metadata ?? TypeMetadataStorage.getUnionsMetadata(); + const unionDefs = unionMetadata.map((item) => + this.unionDefinitionFactory.create(item), ); this.typeDefinitionsStorage.addUnions(unionDefs); } diff --git a/packages/graphql/lib/type-factories/create-union-type.factory.ts b/packages/graphql/lib/type-factories/create-union-type.factory.ts index ddacb5d82..e172e9799 100644 --- a/packages/graphql/lib/type-factories/create-union-type.factory.ts +++ b/packages/graphql/lib/type-factories/create-union-type.factory.ts @@ -7,6 +7,7 @@ import { Type } from '@nestjs/common'; import { ResolveTypeFn } from '../interfaces/resolve-type-fn.interface'; +import { RegisterInOption } from '../schema-builder/metadata'; import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; @@ -32,6 +33,13 @@ export interface UnionOptions< * Types that the union consist of. */ types: () => T; + /** + * NestJS module that this union belongs to. + * When specified, this union will only be included in GraphQL schemas + * that include this module via the `include` option. + * @see RegisterInOption for details + */ + registerIn?: RegisterInOption; } export type ArrayElement = @@ -45,7 +53,7 @@ export type Union = InstanceType>; export function createUnionType< T extends readonly Type[] = Type[], >(options: UnionOptions): Union { - const { name, description, types, resolveType } = options; + const { name, description, types, resolveType, registerIn } = options; const id = Symbol(name); LazyMetadataStorage.store(() => @@ -55,6 +63,7 @@ export function createUnionType< description, typesFn: types, resolveType, + registerIn, }), ); return id as any; diff --git a/packages/graphql/lib/type-factories/register-enum-type.factory.ts b/packages/graphql/lib/type-factories/register-enum-type.factory.ts index c18a4bcbe..813ef3a3e 100644 --- a/packages/graphql/lib/type-factories/register-enum-type.factory.ts +++ b/packages/graphql/lib/type-factories/register-enum-type.factory.ts @@ -5,7 +5,10 @@ * To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql". */ -import { EnumMetadataValuesMap } from '../schema-builder/metadata'; +import { + EnumMetadataValuesMap, + RegisterInOption, +} from '../schema-builder/metadata'; import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage'; import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage'; @@ -25,6 +28,13 @@ export interface EnumOptions { * A map of options for the values of the enum. */ valuesMap?: EnumMetadataValuesMap; + /** + * NestJS module that this enum belongs to. + * When specified, this enum will only be included in GraphQL schemas + * that include this module via the `include` option. + * @see RegisterInOption for details + */ + registerIn?: RegisterInOption; } /** @@ -41,6 +51,7 @@ export function registerEnumType( name: options.name, description: options.description, valuesMap: options.valuesMap || {}, + registerIn: options.registerIn, }), ); } diff --git a/packages/graphql/tests/decorators/register-in.decorator.spec.ts b/packages/graphql/tests/decorators/register-in.decorator.spec.ts new file mode 100644 index 000000000..c3e6e3b6b --- /dev/null +++ b/packages/graphql/tests/decorators/register-in.decorator.spec.ts @@ -0,0 +1,279 @@ +import { + ObjectType, + InputType, + InterfaceType, + ArgsType, + Field, + registerEnumType, + createUnionType, + TypeMetadataStorage, +} from '../../lib'; +import { LazyMetadataStorage } from '../../lib/schema-builder/storages/lazy-metadata.storage'; + +/** + * Integration tests for the registerIn decorator option. + * + * These tests verify that: + * 1. The registerIn option is correctly passed through decorators to metadata storage + * 2. Both direct module references and arrow functions work correctly + * 3. Arrow functions properly resolve to avoid circular dependency issues + */ + +// Mock modules +class AppModule {} +class FeatureModule {} + +describe('registerIn decorator option', () => { + afterEach(() => { + TypeMetadataStorage.clear(); + }); + + describe('@ObjectType decorator', () => { + it('should store registerIn as arrow function', () => { + @ObjectType({ registerIn: () => FeatureModule }) + class TestType { + @Field(() => String) + name!: string; + } + + LazyMetadataStorage.load([TestType]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getObjectTypeMetadataByTarget(TestType); + expect(metadata).toBeDefined(); + expect(metadata!.registerIn).toBeDefined(); + + // Arrow function should be stored + expect(typeof metadata!.registerIn).toBe('function'); + + // When called, it should return the module + const resolvedModule = (metadata!.registerIn as () => Function)(); + expect(resolvedModule).toBe(FeatureModule); + }); + + it('should store registerIn as direct module reference', () => { + @ObjectType({ registerIn: AppModule }) + class TestType { + @Field(() => String) + name!: string; + } + + LazyMetadataStorage.load([TestType]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getObjectTypeMetadataByTarget(TestType); + expect(metadata).toBeDefined(); + expect(metadata!.registerIn).toBe(AppModule); + }); + + it('should work without registerIn option (included in all schemas)', () => { + @ObjectType() + class TestType { + @Field(() => String) + name!: string; + } + + LazyMetadataStorage.load([TestType]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getObjectTypeMetadataByTarget(TestType); + expect(metadata).toBeDefined(); + expect(metadata!.registerIn).toBeUndefined(); + }); + + it('should work with name and registerIn option', () => { + @ObjectType('CustomName', { registerIn: () => FeatureModule }) + class TestType { + @Field(() => String) + name!: string; + } + + LazyMetadataStorage.load([TestType]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getObjectTypeMetadataByTarget(TestType); + expect(metadata).toBeDefined(); + expect(metadata!.name).toBe('CustomName'); + expect((metadata!.registerIn as () => Function)()).toBe(FeatureModule); + }); + }); + + describe('@InputType decorator', () => { + it('should store registerIn option', () => { + @InputType({ registerIn: () => FeatureModule }) + class TestInput { + @Field(() => String) + name!: string; + } + + LazyMetadataStorage.load([TestInput]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getInputTypeMetadataByTarget(TestInput); + expect(metadata).toBeDefined(); + expect((metadata!.registerIn as () => Function)()).toBe(FeatureModule); + }); + + it('should work with name and registerIn option', () => { + @InputType('CustomInput', { registerIn: () => AppModule }) + class TestInput { + @Field(() => String) + value!: string; + } + + LazyMetadataStorage.load([TestInput]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getInputTypeMetadataByTarget(TestInput); + expect(metadata).toBeDefined(); + expect(metadata!.name).toBe('CustomInput'); + expect((metadata!.registerIn as () => Function)()).toBe(AppModule); + }); + }); + + describe('@InterfaceType decorator', () => { + it('should store registerIn option', () => { + @InterfaceType({ registerIn: () => FeatureModule }) + abstract class TestInterface { + @Field(() => String) + id!: string; + } + + LazyMetadataStorage.load([TestInterface]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getInterfaceMetadataByTarget(TestInterface); + expect(metadata).toBeDefined(); + expect((metadata!.registerIn as () => Function)()).toBe(FeatureModule); + }); + }); + + describe('@ArgsType decorator', () => { + it('should store registerIn option', () => { + @ArgsType({ registerIn: () => AppModule }) + class TestArgs { + @Field(() => String) + query!: string; + } + + LazyMetadataStorage.load([TestArgs]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getArgumentsMetadataByTarget(TestArgs); + expect(metadata).toBeDefined(); + expect((metadata!.registerIn as () => Function)()).toBe(AppModule); + }); + }); + + describe('registerEnumType function', () => { + it('should store registerIn option', () => { + enum TestStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', + } + + registerEnumType(TestStatus, { + name: 'TestStatus', + registerIn: () => FeatureModule, + }); + + LazyMetadataStorage.load([]); + TypeMetadataStorage.compile(); + + const allEnums = TypeMetadataStorage.getEnumsMetadata(); + const metadata = allEnums.find((e) => e.name === 'TestStatus'); + + expect(metadata).toBeDefined(); + expect((metadata!.registerIn as () => Function)()).toBe(FeatureModule); + }); + + it('should work with direct module reference', () => { + enum DirectRefStatus { + YES = 'YES', + NO = 'NO', + } + + registerEnumType(DirectRefStatus, { + name: 'DirectRefStatus', + registerIn: AppModule, + }); + + LazyMetadataStorage.load([]); + TypeMetadataStorage.compile(); + + const allEnums = TypeMetadataStorage.getEnumsMetadata(); + const metadata = allEnums.find((e) => e.name === 'DirectRefStatus'); + + expect(metadata).toBeDefined(); + expect(metadata!.registerIn).toBe(AppModule); + }); + }); + + describe('createUnionType function', () => { + it('should store registerIn option', () => { + @ObjectType() + class UnionMemberA { + @Field(() => String) + a!: string; + } + + @ObjectType() + class UnionMemberB { + @Field(() => String) + b!: string; + } + + LazyMetadataStorage.load([UnionMemberA, UnionMemberB]); + + const TestUnion = createUnionType({ + name: 'TestUnion', + types: () => [UnionMemberA, UnionMemberB] as const, + registerIn: () => FeatureModule, + }); + + LazyMetadataStorage.load([]); + TypeMetadataStorage.compile(); + + const allUnions = TypeMetadataStorage.getUnionsMetadata(); + const metadata = allUnions.find((u) => u.name === 'TestUnion'); + + expect(metadata).toBeDefined(); + expect((metadata!.registerIn as () => Function)()).toBe(FeatureModule); + }); + }); + + describe('circular dependency handling', () => { + it('should handle deferred module resolution via arrow function', () => { + // Simulate circular dependency scenario where module is not yet defined + let DeferredModule: any; + + @ObjectType({ registerIn: () => DeferredModule }) + class DeferredType { + @Field(() => String) + name!: string; + } + + // Define module after decorator is applied + DeferredModule = class DeferredModuleClass {}; + + LazyMetadataStorage.load([DeferredType]); + TypeMetadataStorage.compile(); + + const metadata = + TypeMetadataStorage.getObjectTypeMetadataByTarget(DeferredType); + expect(metadata).toBeDefined(); + + // Arrow function should correctly resolve the module + const resolvedModule = (metadata!.registerIn as () => Function)(); + expect(resolvedModule).toBe(DeferredModule); + }); + }); +}); diff --git a/packages/graphql/tests/schema-builder/storages/type-metadata-storage.register-in.spec.ts b/packages/graphql/tests/schema-builder/storages/type-metadata-storage.register-in.spec.ts new file mode 100644 index 000000000..fc1cc2c60 --- /dev/null +++ b/packages/graphql/tests/schema-builder/storages/type-metadata-storage.register-in.spec.ts @@ -0,0 +1,380 @@ +import { + ObjectType, + InputType, + InterfaceType, + ArgsType, + Field, + registerEnumType, + createUnionType, + TypeMetadataStorage, +} from '../../../lib'; +import { LazyMetadataStorage } from '../../../lib/schema-builder/storages/lazy-metadata.storage'; + +// Mock modules for testing +class ModuleA {} +class ModuleB {} +class ModuleC {} + +// Test types for ObjectType +@ObjectType({ registerIn: () => ModuleA }) +class ObjectTypeInModuleA { + @Field(() => String) + name!: string; +} + +@ObjectType({ registerIn: () => ModuleB }) +class ObjectTypeInModuleB { + @Field(() => String) + name!: string; +} + +@ObjectType({ registerIn: ModuleA }) // Direct reference (not arrow function) +class ObjectTypeInModuleADirect { + @Field(() => String) + name!: string; +} + +@ObjectType() // No registerIn - should be included everywhere +class ObjectTypeGlobal { + @Field(() => String) + name!: string; +} + +// Test types for InputType +@InputType({ registerIn: () => ModuleA }) +class InputTypeInModuleA { + @Field(() => String) + name!: string; +} + +@InputType({ registerIn: () => ModuleB }) +class InputTypeInModuleB { + @Field(() => String) + name!: string; +} + +@InputType() // No registerIn +class InputTypeGlobal { + @Field(() => String) + name!: string; +} + +// Test types for InterfaceType +@InterfaceType({ registerIn: () => ModuleA }) +abstract class InterfaceTypeInModuleA { + @Field(() => String) + name!: string; +} + +@InterfaceType({ registerIn: () => ModuleB }) +abstract class InterfaceTypeInModuleB { + @Field(() => String) + name!: string; +} + +@InterfaceType() // No registerIn +abstract class InterfaceTypeGlobal { + @Field(() => String) + name!: string; +} + +// Test types for ArgsType +@ArgsType({ registerIn: () => ModuleA }) +class ArgsTypeInModuleA { + @Field(() => String) + name!: string; +} + +@ArgsType({ registerIn: () => ModuleB }) +class ArgsTypeInModuleB { + @Field(() => String) + name!: string; +} + +@ArgsType() // No registerIn +class ArgsTypeGlobal { + @Field(() => String) + name!: string; +} + +// Test enums +enum StatusEnumA { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', +} + +enum StatusEnumB { + PENDING = 'PENDING', + COMPLETED = 'COMPLETED', +} + +enum StatusEnumGlobal { + YES = 'YES', + NO = 'NO', +} + +registerEnumType(StatusEnumA, { + name: 'StatusEnumA', + registerIn: () => ModuleA, +}); + +registerEnumType(StatusEnumB, { + name: 'StatusEnumB', + registerIn: () => ModuleB, +}); + +registerEnumType(StatusEnumGlobal, { + name: 'StatusEnumGlobal', +}); + +// Test unions - created after loading metadata to ensure ObjectTypes are registered +let UnionInModuleA: any; +let UnionInModuleB: any; +let UnionGlobal: any; + +describe('TypeMetadataStorage registerIn filtering', () => { + beforeAll(() => { + // Load all decorated class metadata + LazyMetadataStorage.load([ + ObjectTypeInModuleA, + ObjectTypeInModuleB, + ObjectTypeInModuleADirect, + ObjectTypeGlobal, + InputTypeInModuleA, + InputTypeInModuleB, + InputTypeGlobal, + InterfaceTypeInModuleA, + InterfaceTypeInModuleB, + InterfaceTypeGlobal, + ArgsTypeInModuleA, + ArgsTypeInModuleB, + ArgsTypeGlobal, + ]); + + // Create unions after ObjectTypes are registered + UnionInModuleA = createUnionType({ + name: 'UnionInModuleA', + types: () => [ObjectTypeInModuleA, ObjectTypeGlobal] as const, + registerIn: () => ModuleA, + }); + + UnionInModuleB = createUnionType({ + name: 'UnionInModuleB', + types: () => [ObjectTypeInModuleB, ObjectTypeGlobal] as const, + registerIn: () => ModuleB, + }); + + UnionGlobal = createUnionType({ + name: 'UnionGlobal', + types: () => [ObjectTypeGlobal] as const, + }); + + // Load enum and union metadata + LazyMetadataStorage.load([]); + + TypeMetadataStorage.compile(); + }); + + afterAll(() => { + TypeMetadataStorage.clear(); + }); + + describe('getObjectTypesMetadataByModules', () => { + it('should return types registered in specified module', () => { + const result = TypeMetadataStorage.getObjectTypesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeInModuleA); + expect(targets).toContain(ObjectTypeInModuleADirect); + expect(targets).not.toContain(ObjectTypeInModuleB); + }); + + it('should include types with no registerIn (global types)', () => { + const result = TypeMetadataStorage.getObjectTypesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeGlobal); + }); + + it('should handle multiple modules', () => { + const result = TypeMetadataStorage.getObjectTypesMetadataByModules([ + ModuleA, + ModuleB, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeInModuleA); + expect(targets).toContain(ObjectTypeInModuleB); + expect(targets).toContain(ObjectTypeGlobal); + }); + + it('should return only global types when module has no registered types', () => { + const result = TypeMetadataStorage.getObjectTypesMetadataByModules([ + ModuleC, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeGlobal); + expect(targets).not.toContain(ObjectTypeInModuleA); + expect(targets).not.toContain(ObjectTypeInModuleB); + }); + + it('should handle direct module reference (not arrow function)', () => { + const result = TypeMetadataStorage.getObjectTypesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeInModuleADirect); + }); + }); + + describe('getInputTypesMetadataByModules', () => { + it('should return input types registered in specified module', () => { + const result = TypeMetadataStorage.getInputTypesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(InputTypeInModuleA); + expect(targets).not.toContain(InputTypeInModuleB); + }); + + it('should include input types with no registerIn', () => { + const result = TypeMetadataStorage.getInputTypesMetadataByModules([ + ModuleB, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(InputTypeGlobal); + expect(targets).toContain(InputTypeInModuleB); + }); + }); + + describe('getInterfacesMetadataByModules', () => { + it('should return interface types registered in specified module', () => { + const result = TypeMetadataStorage.getInterfacesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(InterfaceTypeInModuleA); + expect(targets).not.toContain(InterfaceTypeInModuleB); + }); + + it('should include interface types with no registerIn', () => { + const result = TypeMetadataStorage.getInterfacesMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(InterfaceTypeGlobal); + }); + }); + + describe('getArgumentsMetadataByModules', () => { + it('should return args types registered in specified module', () => { + const result = TypeMetadataStorage.getArgumentsMetadataByModules([ + ModuleA, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ArgsTypeInModuleA); + expect(targets).not.toContain(ArgsTypeInModuleB); + }); + + it('should include args types with no registerIn', () => { + const result = TypeMetadataStorage.getArgumentsMetadataByModules([ + ModuleB, + ]); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ArgsTypeGlobal); + }); + }); + + describe('getEnumsMetadataByModules', () => { + it('should return enums registered in specified module', () => { + const result = TypeMetadataStorage.getEnumsMetadataByModules([ModuleA]); + + const refs = result.map((m) => m.ref); + expect(refs).toContain(StatusEnumA); + expect(refs).not.toContain(StatusEnumB); + }); + + it('should include enums with no registerIn', () => { + const result = TypeMetadataStorage.getEnumsMetadataByModules([ModuleA]); + + const refs = result.map((m) => m.ref); + expect(refs).toContain(StatusEnumGlobal); + }); + + it('should filter correctly for ModuleB', () => { + const result = TypeMetadataStorage.getEnumsMetadataByModules([ModuleB]); + + const refs = result.map((m) => m.ref); + expect(refs).toContain(StatusEnumB); + expect(refs).toContain(StatusEnumGlobal); + expect(refs).not.toContain(StatusEnumA); + }); + }); + + describe('getUnionsMetadataByModules', () => { + it('should return unions registered in specified module', () => { + const result = TypeMetadataStorage.getUnionsMetadataByModules([ModuleA]); + + const names = result.map((m) => m.name); + expect(names).toContain('UnionInModuleA'); + expect(names).not.toContain('UnionInModuleB'); + }); + + it('should include unions with no registerIn', () => { + const result = TypeMetadataStorage.getUnionsMetadataByModules([ModuleA]); + + const names = result.map((m) => m.name); + expect(names).toContain('UnionGlobal'); + }); + + it('should filter correctly for ModuleB', () => { + const result = TypeMetadataStorage.getUnionsMetadataByModules([ModuleB]); + + const names = result.map((m) => m.name); + expect(names).toContain('UnionInModuleB'); + expect(names).toContain('UnionGlobal'); + expect(names).not.toContain('UnionInModuleA'); + }); + }); + + describe('default behavior (all types included when no module filter)', () => { + it('should return all types when using getObjectTypesMetadata (no module filter)', () => { + const result = TypeMetadataStorage.getObjectTypesMetadata(); + + const targets = result.map((m) => m.target); + expect(targets).toContain(ObjectTypeInModuleA); + expect(targets).toContain(ObjectTypeInModuleB); + expect(targets).toContain(ObjectTypeGlobal); + }); + + it('should return all input types when using getInputTypesMetadata (no module filter)', () => { + const result = TypeMetadataStorage.getInputTypesMetadata(); + + const targets = result.map((m) => m.target); + expect(targets).toContain(InputTypeInModuleA); + expect(targets).toContain(InputTypeInModuleB); + expect(targets).toContain(InputTypeGlobal); + }); + + it('should return all enums when using getEnumsMetadata (no module filter)', () => { + const result = TypeMetadataStorage.getEnumsMetadata(); + + const refs = result.map((m) => m.ref); + expect(refs).toContain(StatusEnumA); + expect(refs).toContain(StatusEnumB); + expect(refs).toContain(StatusEnumGlobal); + }); + }); +}); diff --git a/packages/graphql/tests/schema-builder/type-definitions-generator.register-in.spec.ts b/packages/graphql/tests/schema-builder/type-definitions-generator.register-in.spec.ts new file mode 100644 index 000000000..08a059dd0 --- /dev/null +++ b/packages/graphql/tests/schema-builder/type-definitions-generator.register-in.spec.ts @@ -0,0 +1,342 @@ +import { Test } from '@nestjs/testing'; +import { + ObjectType, + InputType, + InterfaceType, + Field, + registerEnumType, + createUnionType, + TypeMetadataStorage, + GraphQLSchemaBuilderModule, +} from '../../lib'; +import { TypeDefinitionsGenerator } from '../../lib/schema-builder/type-definitions.generator'; +import { TypeDefinitionsStorage } from '../../lib/schema-builder/storages/type-definitions.storage'; +import { LazyMetadataStorage } from '../../lib/schema-builder/storages/lazy-metadata.storage'; + +// Mock modules for testing +class UsersModule {} +class ProductsModule {} + +// Types for UsersModule +@ObjectType({ registerIn: () => UsersModule }) +class User { + @Field(() => String) + id!: string; + + @Field(() => String) + name!: string; +} + +@InputType({ registerIn: () => UsersModule }) +class CreateUserInput { + @Field(() => String) + name!: string; +} + +@InterfaceType({ registerIn: () => UsersModule }) +abstract class UserInterface { + @Field(() => String) + id!: string; +} + +// Types for ProductsModule +@ObjectType({ registerIn: () => ProductsModule }) +class Product { + @Field(() => String) + id!: string; + + @Field(() => String) + name!: string; +} + +@InputType({ registerIn: () => ProductsModule }) +class CreateProductInput { + @Field(() => String) + name!: string; +} + +@InterfaceType({ registerIn: () => ProductsModule }) +abstract class ProductInterface { + @Field(() => String) + id!: string; +} + +// Global types (no registerIn) +@ObjectType() +class GlobalEntity { + @Field(() => String) + id!: string; +} + +@InputType() +class GlobalInput { + @Field(() => String) + value!: string; +} + +// Enums +enum UserStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', +} + +enum ProductStatus { + AVAILABLE = 'AVAILABLE', + SOLD_OUT = 'SOLD_OUT', +} + +enum GlobalStatus { + YES = 'YES', + NO = 'NO', +} + +registerEnumType(UserStatus, { + name: 'UserStatus', + registerIn: () => UsersModule, +}); + +registerEnumType(ProductStatus, { + name: 'ProductStatus', + registerIn: () => ProductsModule, +}); + +registerEnumType(GlobalStatus, { + name: 'GlobalStatus', +}); + +// Unions - will be created in beforeAll after ObjectTypes are loaded +let UserUnion: any; +let ProductUnion: any; + +describe('TypeDefinitionsGenerator with includeModules', () => { + let generator: TypeDefinitionsGenerator; + let storage: TypeDefinitionsStorage; + + beforeAll(async () => { + // Load all decorated class metadata + LazyMetadataStorage.load([ + User, + CreateUserInput, + UserInterface, + Product, + CreateProductInput, + ProductInterface, + GlobalEntity, + GlobalInput, + ]); + + // Create unions after ObjectTypes are loaded + UserUnion = createUnionType({ + name: 'UserUnion', + types: () => [User, GlobalEntity] as const, + registerIn: () => UsersModule, + }); + + ProductUnion = createUnionType({ + name: 'ProductUnion', + types: () => [Product, GlobalEntity] as const, + registerIn: () => ProductsModule, + }); + + // Load remaining metadata + LazyMetadataStorage.load([]); + + TypeMetadataStorage.compile(); + + const moduleRef = await Test.createTestingModule({ + imports: [GraphQLSchemaBuilderModule], + }).compile(); + + generator = moduleRef.get(TypeDefinitionsGenerator); + storage = moduleRef.get(TypeDefinitionsStorage); + }); + + afterAll(() => { + TypeMetadataStorage.clear(); + }); + + describe('when includeModules is specified', () => { + describe('UsersModule only', () => { + beforeEach(() => { + generator.generate({}, [UsersModule]); + }); + + it('should include ObjectTypes registered in UsersModule', () => { + const userType = storage.getObjectTypeByTarget(User); + expect(userType).toBeDefined(); + }); + + it('should include global ObjectTypes (no registerIn)', () => { + const globalType = storage.getObjectTypeByTarget(GlobalEntity); + expect(globalType).toBeDefined(); + }); + + it('should NOT include ObjectTypes registered in ProductsModule', () => { + const productType = storage.getObjectTypeByTarget(Product); + expect(productType).toBeUndefined(); + }); + + it('should include InputTypes registered in UsersModule', () => { + const inputType = storage.getInputTypeByTarget(CreateUserInput); + expect(inputType).toBeDefined(); + }); + + it('should NOT include InputTypes registered in ProductsModule', () => { + const inputType = storage.getInputTypeByTarget(CreateProductInput); + expect(inputType).toBeUndefined(); + }); + + it('should include InterfaceTypes registered in UsersModule', () => { + const interfaceType = storage.getInterfaceByTarget(UserInterface); + expect(interfaceType).toBeDefined(); + }); + + it('should NOT include InterfaceTypes registered in ProductsModule', () => { + const interfaceType = storage.getInterfaceByTarget(ProductInterface); + expect(interfaceType).toBeUndefined(); + }); + + it('should include enums registered in UsersModule', () => { + const enumDef = storage.getEnumByObject(UserStatus); + expect(enumDef).toBeDefined(); + }); + + it('should include global enums', () => { + const enumDef = storage.getEnumByObject(GlobalStatus); + expect(enumDef).toBeDefined(); + }); + + it('should NOT include enums registered in ProductsModule', () => { + const enumDef = storage.getEnumByObject(ProductStatus); + expect(enumDef).toBeUndefined(); + }); + }); + + describe('ProductsModule only', () => { + beforeEach(() => { + generator.generate({}, [ProductsModule]); + }); + + it('should include ObjectTypes registered in ProductsModule', () => { + const productType = storage.getObjectTypeByTarget(Product); + expect(productType).toBeDefined(); + }); + + it('should NOT include ObjectTypes registered in UsersModule', () => { + const userType = storage.getObjectTypeByTarget(User); + expect(userType).toBeUndefined(); + }); + + it('should include global ObjectTypes', () => { + const globalType = storage.getObjectTypeByTarget(GlobalEntity); + expect(globalType).toBeDefined(); + }); + + it('should include InputTypes registered in ProductsModule', () => { + const inputType = storage.getInputTypeByTarget(CreateProductInput); + expect(inputType).toBeDefined(); + }); + + it('should include global InputTypes', () => { + const inputType = storage.getInputTypeByTarget(GlobalInput); + expect(inputType).toBeDefined(); + }); + }); + + describe('Both modules', () => { + beforeEach(() => { + generator.generate({}, [UsersModule, ProductsModule]); + }); + + it('should include ObjectTypes from both modules', () => { + const userType = storage.getObjectTypeByTarget(User); + const productType = storage.getObjectTypeByTarget(Product); + + expect(userType).toBeDefined(); + expect(productType).toBeDefined(); + }); + + it('should include InputTypes from both modules', () => { + const userInput = storage.getInputTypeByTarget(CreateUserInput); + const productInput = storage.getInputTypeByTarget(CreateProductInput); + + expect(userInput).toBeDefined(); + expect(productInput).toBeDefined(); + }); + + it('should include global types', () => { + const globalType = storage.getObjectTypeByTarget(GlobalEntity); + const globalInput = storage.getInputTypeByTarget(GlobalInput); + + expect(globalType).toBeDefined(); + expect(globalInput).toBeDefined(); + }); + + it('should include enums from both modules and global', () => { + const userEnum = storage.getEnumByObject(UserStatus); + const productEnum = storage.getEnumByObject(ProductStatus); + const globalEnum = storage.getEnumByObject(GlobalStatus); + + expect(userEnum).toBeDefined(); + expect(productEnum).toBeDefined(); + expect(globalEnum).toBeDefined(); + }); + }); + }); + + describe('when includeModules is not specified (all types included)', () => { + beforeEach(() => { + generator.generate({}); + }); + + it('should include all ObjectTypes', () => { + const userType = storage.getObjectTypeByTarget(User); + const productType = storage.getObjectTypeByTarget(Product); + const globalType = storage.getObjectTypeByTarget(GlobalEntity); + + expect(userType).toBeDefined(); + expect(productType).toBeDefined(); + expect(globalType).toBeDefined(); + }); + + it('should include all InputTypes', () => { + const userInput = storage.getInputTypeByTarget(CreateUserInput); + const productInput = storage.getInputTypeByTarget(CreateProductInput); + const globalInput = storage.getInputTypeByTarget(GlobalInput); + + expect(userInput).toBeDefined(); + expect(productInput).toBeDefined(); + expect(globalInput).toBeDefined(); + }); + + it('should include all enums', () => { + const userEnum = storage.getEnumByObject(UserStatus); + const productEnum = storage.getEnumByObject(ProductStatus); + const globalEnum = storage.getEnumByObject(GlobalStatus); + + expect(userEnum).toBeDefined(); + expect(productEnum).toBeDefined(); + expect(globalEnum).toBeDefined(); + }); + }); + + describe('TypeDefinitionsStorage.clear() functionality', () => { + it('should clear previous definitions when generating new schema', () => { + // First generate with UsersModule + generator.generate({}, [UsersModule]); + const userTypeFirst = storage.getObjectTypeByTarget(User); + expect(userTypeFirst).toBeDefined(); + + // Then generate with ProductsModule only + generator.generate({}, [ProductsModule]); + + // User should no longer be present + const userTypeSecond = storage.getObjectTypeByTarget(User); + expect(userTypeSecond).toBeUndefined(); + + // Product should now be present + const productType = storage.getObjectTypeByTarget(Product); + expect(productType).toBeDefined(); + }); + }); +});