diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts index 9b426a5..596e5bc 100644 --- a/packages/core/src/lib/errors.ts +++ b/packages/core/src/lib/errors.ts @@ -1,4 +1,5 @@ import { Type } from '@deepkit/type'; +import { AbstractClassType } from '@deepkit/core'; export class TypeNameRequiredError extends Error { constructor(readonly type: Type) { @@ -15,7 +16,19 @@ export class UnknownTypeNameError extends Error { export class InvalidSubscriptionTypeError extends Error { constructor(readonly type: Type, className: string, methodName: string) { super( - `The return type of "${methodName}" method on "${className}" class must be AsyncGenerator, AsyncIterable, Observable or BrokerBus when @graphql.subscription() decorator is used`, + `The return type of "${methodName}" method on "${className}" class must be one of AsyncGenerator, AsyncIterable, Observable or BrokerBusChannel when @graphql.subscription() decorator is used`, ); } } + +export class MissingTypeArgumentError extends Error { + constructor(readonly type: Type) { + super(`Missing type argument for ${type.typeName}`); + } +} + +export class MissingResolverDecoratorError extends Error { + constructor(readonly classType: AbstractClassType) { + super(`Missing @graphql.resolver() decorator on ${classType.name}`); + } +} diff --git a/packages/core/src/lib/types-builder.spec.ts b/packages/core/src/lib/types-builder.spec.ts index 3715818..066b5b6 100644 --- a/packages/core/src/lib/types-builder.spec.ts +++ b/packages/core/src/lib/types-builder.spec.ts @@ -7,6 +7,7 @@ import { } from '@deepkit/broker'; import { BehaviorSubject, Observable } from 'rxjs'; import { + Excluded, float, float32, float64, @@ -654,6 +655,44 @@ describe('TypesBuilder', () => { `); }); + test('excluded properties', () => { + interface User { + readonly id: string; + readonly username: string; + readonly password: string & Excluded; + } + + const userObjectType = + builder.createOutputType() as GraphQLObjectType; + + expect(userObjectType.getFields()).toMatchInlineSnapshot(` + { + "id": { + "args": [], + "astNode": undefined, + "deprecationReason": undefined, + "description": undefined, + "extensions": {}, + "name": "id", + "resolve": undefined, + "subscribe": undefined, + "type": "String!", + }, + "username": { + "args": [], + "astNode": undefined, + "deprecationReason": undefined, + "description": undefined, + "extensions": {}, + "name": "username", + "resolve": undefined, + "subscribe": undefined, + "type": "String!", + }, + } + `); + }); + test('circular references', () => { interface Post { readonly id: string; diff --git a/packages/core/src/lib/types-builder.ts b/packages/core/src/lib/types-builder.ts index fc53cdf..92ea9e1 100644 --- a/packages/core/src/lib/types-builder.ts +++ b/packages/core/src/lib/types-builder.ts @@ -73,6 +73,7 @@ import { filterReflectionParametersMetaAnnotationsForArguments, getClassDecoratorMetadata, getContextMetaAnnotationReflectionParameterIndex, + getNonExcludedReflectionClassProperties, getParentMetaAnnotationReflectionParameterIndex, getTypeName, isAsyncIterable, @@ -419,9 +420,13 @@ export class TypesBuilder { const resolver = this.getResolver(type); + const reflectionClassProperties = + getNonExcludedReflectionClassProperties(reflectionClass); + return Object.fromEntries( - reflectionClass.getProperties().map(property => { + reflectionClassProperties.map(property => { let type = this.createOutputType(property.type); + if (!property.isOptional() && !property.isNullable()) { type = new GraphQLNonNull(type); } @@ -690,7 +695,9 @@ export class TypesBuilder { if (type === 'subscription') { if (result instanceof BrokerBusChannel) { const observable = new Observable(subscriber => { - result.subscribe((value: unknown) => subscriber.next(serializeResult(value))); + result.subscribe((value: unknown) => + subscriber.next(serializeResult(value)), + ); }); return observableToAsyncIterable(observable); } diff --git a/packages/core/src/lib/utils.spec.ts b/packages/core/src/lib/utils.spec.ts index 2093e00..63441f5 100644 --- a/packages/core/src/lib/utils.spec.ts +++ b/packages/core/src/lib/utils.spec.ts @@ -1,6 +1,6 @@ -import { integer, typeOf } from '@deepkit/type'; +import { Excluded, integer, ReflectionClass, typeOf } from '@deepkit/type'; -import { getTypeName } from './utils'; +import { getNonExcludedReflectionClassProperties, getTypeName } from './utils'; describe('getTypeName', () => { test('union', () => { @@ -42,3 +42,22 @@ describe('getTypeName', () => { ); }); }); + +test('getNonExcludedReflectionClassProperties', () => { + interface User { + readonly id: string; + readonly username: string; + readonly password: string & Excluded; + } + + const reflectionClass = ReflectionClass.from(); + + expect( + getNonExcludedReflectionClassProperties(reflectionClass).map(p => p.name), + ).toMatchInlineSnapshot(` + [ + "id", + "username", + ] + `); +}); diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts index bc5d40d..452acfd 100644 --- a/packages/core/src/lib/utils.ts +++ b/packages/core/src/lib/utils.ts @@ -1,9 +1,13 @@ import { AbstractClassType } from '@deepkit/core'; import { BrokerBusChannel } from '@deepkit/broker'; +import { Observable } from 'rxjs'; import { metaAnnotation, + ReflectionClass, ReflectionKind, + excludedAnnotation, ReflectionParameter, + ReflectionProperty, stringifyType, Type, TypeClass, @@ -12,10 +16,13 @@ import { TypeUndefined, } from '@deepkit/type'; -import { UnknownTypeNameError } from './errors'; +import { + MissingResolverDecoratorError, + MissingTypeArgumentError, + UnknownTypeNameError, +} from './errors'; import { CONTEXT_META_NAME, PARENT_META_NAME } from './types'; import { gqlClassDecorator, GraphQLClassMetadata } from './decorators'; -import { Observable } from 'rxjs'; export function isAsyncIterable(obj: unknown): obj is AsyncIterable { return obj != null && typeof obj === 'object' && Symbol.asyncIterator in obj; @@ -99,48 +106,22 @@ export function excludeNullAndUndefinedTypes( ) as readonly Exclude[]; } +export function getTypeArgument(type: Type): Type { + const typeArgument = type.typeArguments?.[0]; + if (!typeArgument) { + throw new MissingTypeArgumentError(type); + } + return typeArgument; +} + export function maybeUnwrapSubscriptionReturnType(type: Type): Type { switch (true) { - case type.typeName === 'Generator': { - const typeArgument = type.typeArguments?.[0]; - if (!typeArgument) { - throw new Error('Missing type argument for Generator'); - } - return typeArgument; - } - - case type.typeName === 'AsyncGenerator': { - const typeArgument = type.typeArguments?.[0]; - if (!typeArgument) { - throw new Error('Missing type argument for AsyncGenerator'); - } - return typeArgument; - } - - case type.typeName === 'AsyncIterable': { - const typeArgument = type.typeArguments?.[0]; - if (!typeArgument) { - throw new Error('Missing type argument for AsyncIterable'); - } - return typeArgument; - } - - - case (type as TypeClass).classType === BrokerBusChannel: { - const typeArgument = type.typeArguments?.[0]; - if (!typeArgument) { - throw new Error('Missing type argument for BrokerBusChannel'); - } - return typeArgument; - } - - case ((type as TypeClass).classType === Observable): { - const typeArgument = type.typeArguments?.[0]; - if (!typeArgument) { - throw new Error('Missing type argument for Observable'); - } - return typeArgument; - } + case type.typeName === 'Generator': + case type.typeName === 'AsyncGenerator': + case type.typeName === 'AsyncIterable': + case (type as TypeClass).classType === BrokerBusChannel: + case (type as TypeClass).classType === Observable: + return getTypeArgument(type); default: return type; @@ -161,14 +142,23 @@ export function raise(error: string): never { throw new Error(error); } +export function getNonExcludedReflectionClassProperties( + reflectionClass: ReflectionClass, +): readonly ReflectionProperty[] { + return reflectionClass + .getProperties() + .filter( + property => + !excludedAnnotation.isExcluded(property.type, property.type.typeName!), + ); +} + export function getClassDecoratorMetadata( classType: AbstractClassType, ): GraphQLClassMetadata { const resolver = gqlClassDecorator._fetch(classType); if (!resolver) { - throw new Error( - `Missing @graphql.resolver() decorator on ${classType.name}`, - ); + throw new MissingResolverDecoratorError(classType); } return resolver; }