Skip to content

Commit

Permalink
feat(core): exclude excluded type properties in generated graphql obj…
Browse files Browse the repository at this point in the history
…ect type
  • Loading branch information
marcus-sa committed Nov 29, 2023
1 parent 8881c17 commit 6009c3d
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 50 deletions.
15 changes: 14 additions & 1 deletion packages/core/src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Type } from '@deepkit/type';
import { AbstractClassType } from '@deepkit/core';

export class TypeNameRequiredError extends Error {
constructor(readonly type: Type) {
Expand All @@ -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<T>, AsyncIterable<T>, Observable<T> or BrokerBus<T> when @graphql.subscription() decorator is used`,
`The return type of "${methodName}" method on "${className}" class must be one of AsyncGenerator<T>, AsyncIterable<T>, Observable<T> or BrokerBusChannel<T> when @graphql.subscription() decorator is used`,
);
}
}

export class MissingTypeArgumentError extends Error {
constructor(readonly type: Type) {
super(`Missing type argument for ${type.typeName}<T>`);
}
}

export class MissingResolverDecoratorError extends Error {
constructor(readonly classType: AbstractClassType) {
super(`Missing @graphql.resolver() decorator on ${classType.name}`);
}
}
39 changes: 39 additions & 0 deletions packages/core/src/lib/types-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@deepkit/broker';
import { BehaviorSubject, Observable } from 'rxjs';
import {
Excluded,
float,
float32,
float64,
Expand Down Expand Up @@ -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<User>() 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;
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/lib/types-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
filterReflectionParametersMetaAnnotationsForArguments,
getClassDecoratorMetadata,
getContextMetaAnnotationReflectionParameterIndex,
getNonExcludedReflectionClassProperties,
getParentMetaAnnotationReflectionParameterIndex,
getTypeName,
isAsyncIterable,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
23 changes: 21 additions & 2 deletions packages/core/src/lib/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -42,3 +42,22 @@ describe('getTypeName', () => {
);
});
});

test('getNonExcludedReflectionClassProperties', () => {
interface User {
readonly id: string;
readonly username: string;
readonly password: string & Excluded;
}

const reflectionClass = ReflectionClass.from<User>();

expect(
getNonExcludedReflectionClassProperties(reflectionClass).map(p => p.name),
).toMatchInlineSnapshot(`
[
"id",
"username",
]
`);
});
80 changes: 35 additions & 45 deletions packages/core/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<unknown> {
return obj != null && typeof obj === 'object' && Symbol.asyncIterator in obj;
Expand Down Expand Up @@ -99,48 +106,22 @@ export function excludeNullAndUndefinedTypes(
) as readonly Exclude<Type, TypeUndefined | TypeNull>[];
}

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<T>');
}
return typeArgument;
}

case type.typeName === 'AsyncGenerator': {
const typeArgument = type.typeArguments?.[0];
if (!typeArgument) {
throw new Error('Missing type argument for AsyncGenerator<T>');
}
return typeArgument;
}

case type.typeName === 'AsyncIterable': {
const typeArgument = type.typeArguments?.[0];
if (!typeArgument) {
throw new Error('Missing type argument for AsyncIterable<T>');
}
return typeArgument;
}


case (type as TypeClass).classType === BrokerBusChannel: {
const typeArgument = type.typeArguments?.[0];
if (!typeArgument) {
throw new Error('Missing type argument for BrokerBusChannel<T>');
}
return typeArgument;
}

case ((type as TypeClass).classType === Observable): {
const typeArgument = type.typeArguments?.[0];
if (!typeArgument) {
throw new Error('Missing type argument for Observable<T>');
}
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;
Expand All @@ -161,14 +142,23 @@ export function raise(error: string): never {
throw new Error(error);
}

export function getNonExcludedReflectionClassProperties<T>(
reflectionClass: ReflectionClass<T>,
): readonly ReflectionProperty[] {
return reflectionClass
.getProperties()
.filter(
property =>
!excludedAnnotation.isExcluded(property.type, property.type.typeName!),
);
}

export function getClassDecoratorMetadata<T>(
classType: AbstractClassType<T>,
): GraphQLClassMetadata {
const resolver = gqlClassDecorator._fetch(classType);
if (!resolver) {
throw new Error(
`Missing @graphql.resolver() decorator on ${classType.name}`,
);
throw new MissingResolverDecoratorError(classType);
}
return resolver;
}

0 comments on commit 6009c3d

Please sign in to comment.