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
19 changes: 19 additions & 0 deletions docs/explore/object-type-and-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,22 @@ class Person {
}
}
```

## Interfaces

```ts
import { InterfaceType, ObjectType, Field } from 'typegql';

@InterfaceType()
class Vehicle {
@Field() id: number;
}

@ObjectType()
class Car extends Vehicle {
@Field({ type: () => Person })
owner() {
return db.findPersonByCarId(this.id);
}
}
```
17 changes: 17 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ interface ObjectTypeOptions {
@ObjectType(options?: ObjectTypeOptions)
```

### @InterfaceType

```typescript
interface InterfaceTypeOptions {
name?: string; // infered from class name
description?: string;
}

@InterfaceType(options?: InterfaceTypeOptions)
```

### @Field

```typescript
Expand Down Expand Up @@ -180,6 +191,12 @@ compileSchema(schemaTarget: Function): GraphQLSchema
compileObjectType(schemaTarget: Function): GraphQLObjectType
```

### compileInterfaceType,

```typescript
compileInterfaceType(schemaTarget: Function): GraphQLInterfaceType
```

### compileInputObjectType,

```typescript
Expand Down
5 changes: 5 additions & 0 deletions src/domains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export {
compileObjectType,
objectTypeRegistry,
} from './objectType';
export {
InterfaceType,
compileInterfaceType,
interfaceTypeRegistry,
} from './interfaceType';
export {
InputObjectType,
compileInputObjectType,
Expand Down
4 changes: 4 additions & 0 deletions src/domains/interfaceType/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
compileInterfaceType,
compileInterfaceTypeWithConfig,
} from './interfaceType';
62 changes: 62 additions & 0 deletions src/domains/interfaceType/compiler/interfaceType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { GraphQLInterfaceType } from 'graphql';
import { InterfaceTypeError, interfaceTypeRegistry } from '../index';

import { compileAllFields, fieldsRegistry } from '~/domains/field';
import {
createCachedThunk,
getClassWithAllParentClasses,
} from '~/services/utils';

const compileOutputTypeCache = new WeakMap<Function, GraphQLInterfaceType>();

export interface TypeOptions {
name: string;
description?: string;
}

function createTypeFieldsGetter(target: Function) {
const targetWithParents = getClassWithAllParentClasses(target);
const hasFields = targetWithParents.some(ancestor => {
return !fieldsRegistry.isEmpty(ancestor);
});

if (!hasFields) {
throw new InterfaceTypeError(
target,
`There are no fields inside this type.`,
);
}

return createCachedThunk(() => {
return compileAllFields(target);
});
}

export function compileInterfaceTypeWithConfig(
target: Function,
config: TypeOptions,
): GraphQLInterfaceType {
if (compileOutputTypeCache.has(target)) {
return compileOutputTypeCache.get(target);
}

const compiled = new GraphQLInterfaceType({
...config,
fields: createTypeFieldsGetter(target),
});

compileOutputTypeCache.set(target, compiled);
return compiled;
}

export function compileInterfaceType(target: Function) {
if (!interfaceTypeRegistry.has(target)) {
throw new InterfaceTypeError(
target,
`Class is not registered. Make sure it's decorated with @ObjectType decorator`,
);
}

const compiler = interfaceTypeRegistry.get(target);
return compiler();
}
9 changes: 9 additions & 0 deletions src/domains/interfaceType/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BaseError } from '~/services/error';

export class InterfaceTypeError extends BaseError {
constructor(target: Function, msg: string) {
const fullMsg = `@InterfaceType '${target.name}': ${msg}`;
super(fullMsg);
this.message = fullMsg;
}
}
20 changes: 20 additions & 0 deletions src/domains/interfaceType/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { compileInterfaceTypeWithConfig } from './compiler';
import { interfaceTypeRegistry } from './registry';

export { compileInterfaceType } from './compiler';
export { InterfaceTypeError } from './error';
export { interfaceTypeRegistry } from './registry';

export interface InterfaceTypeOptions {
name?: string;
description?: string;
}

export function InterfaceType(options?: InterfaceTypeOptions): ClassDecorator {
return (target: Function) => {
const config = { name: target.name, ...options };
const outputTypeCompiler = () =>
compileInterfaceTypeWithConfig(target, config);
interfaceTypeRegistry.set(target, outputTypeCompiler);
};
}
14 changes: 14 additions & 0 deletions src/domains/interfaceType/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { GraphQLInterfaceType, GraphQLTypeResolver } from 'graphql';

type Getter<Result> = () => Result;

export const interfaceTypeRegistry = new WeakMap<
Function,
Getter<GraphQLInterfaceType>
>();

export interface TypeConfig {
name: string;
description: string;
resolveType: GraphQLTypeResolver<any, any> | void;
}
17 changes: 17 additions & 0 deletions src/domains/objectType/compiler/objectType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { GraphQLObjectType } from 'graphql';
import { ObjectTypeError, objectTypeRegistry } from '../index';
import {
compileInterfaceType,
interfaceTypeRegistry,
} from '~/domains/interfaceType';

import { compileAllFields, fieldsRegistry } from '~/domains/field';
import {
Expand All @@ -14,6 +18,18 @@ export interface TypeOptions {
description?: string;
}

function getInterfaces(target: Function) {
const targetWithParents = getClassWithAllParentClasses(target);
const interfaces = targetWithParents.filter(ancestor => {
return interfaceTypeRegistry.has(ancestor);
});

if (interfaces) {
const result = interfaces.map(fn => compileInterfaceType(fn));
return result;
}
}

function createTypeFieldsGetter(target: Function) {
const targetWithParents = getClassWithAllParentClasses(target);
const hasFields = targetWithParents.some(ancestor => {
Expand Down Expand Up @@ -41,6 +57,7 @@ export function compileObjectTypeWithConfig(
...config,
isTypeOf: value => value instanceof target,
fields: createTypeFieldsGetter(target),
interfaces: getInterfaces(target),
});

compileOutputTypeCache.set(target, compiled);
Expand Down
2 changes: 1 addition & 1 deletion src/domains/objectType/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { objectTypeRegistry } from './registry';

export { compileObjectType } from './compiler';
export { ObjectTypeError } from './error';
export { objectTypeRegistry, inputTypeRegistry } from './registry';
export { objectTypeRegistry } from './registry';

export interface ObjectTypeOptions {
name?: string;
Expand Down
6 changes: 1 addition & 5 deletions src/domains/objectType/registry.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { GraphQLInputType, GraphQLObjectType } from 'graphql';
import { GraphQLObjectType } from 'graphql';

type Getter<Result> = () => Result;

export const objectTypeRegistry = new WeakMap<
Function,
Getter<GraphQLObjectType>
>();
export const inputTypeRegistry = new WeakMap<
Function,
Getter<GraphQLInputType>
>();

export interface TypeConfig {
name: string;
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
SchemaRoot,
Context,
ObjectType,
InterfaceType,
Query,
Mutation,
InputField,
Expand Down
7 changes: 7 additions & 0 deletions src/test/interfaceType/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Type Throws when @InterfaceType has no fields 1`] = `"@InterfaceType 'NoFields': There are no fields inside this type."`;

exports[`Type Throws when @InterfaceType has no fields 2`] = `"@InterfaceType 'NoDeclaredFields': There are no fields inside this type."`;

exports[`Type Throws when trying to compile type without @InterfaceType decorator 1`] = `"@InterfaceType 'Bar': Class is not registered. Make sure it's decorated with @ObjectType decorator"`;
75 changes: 75 additions & 0 deletions src/test/interfaceType/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { GraphQLInterfaceType } from 'graphql';
import { InterfaceType, compileInterfaceType } from '~/domains';
import { Field } from '~/domains/field';

describe('Type', () => {
it('Throws when trying to compile type without @InterfaceType decorator', () => {
expect(() =>
compileInterfaceType(class Bar {}),
).toThrowErrorMatchingSnapshot();
});

it('Throws when @InterfaceType has no fields', () => {
@InterfaceType()
class NoFields {}

@InterfaceType()
class NoDeclaredFields {
foo: string;
}
expect(() => compileInterfaceType(NoFields)).toThrowErrorMatchingSnapshot();
expect(() =>
compileInterfaceType(NoDeclaredFields),
).toThrowErrorMatchingSnapshot();
});

it('Compiles basic type with field', () => {
@InterfaceType()
class Foo {
@Field() bar: string;
}

const compiled = compileInterfaceType(Foo);

const fields = compiled.getFields();
const barField = fields.bar;

expect(compiled).toBeInstanceOf(GraphQLInterfaceType);

expect(barField).toBeTruthy();
expect(barField.name).toEqual('bar');
});

it('Sets proper options', () => {
@InterfaceType({ description: 'Baz' })
class Foo {
@Field() bar: string;
}

const compiled = compileInterfaceType(Foo);

expect(compiled.description).toEqual('Baz');
expect(compiled.name).toEqual('Foo');

@InterfaceType({ name: 'Baz' })
class FooCustomName {
@Field() bar: string;
}

const compiledCustomName = compileInterfaceType(FooCustomName);

expect(compiledCustomName.name).toEqual('Baz');
});

it('Final type is compiled only once per class', () => {
@InterfaceType()
class Foo {
@Field() bar: string;
}

const compiledA = compileInterfaceType(Foo);
const compiledB = compileInterfaceType(Foo);

expect(compiledA).toBe(compiledB);
});
});
61 changes: 61 additions & 0 deletions src/test/interfaceType/inheritance.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { InterfaceType, compileInterfaceType, Field } from '~/domains';
import { getClassWithAllParentClasses } from '~/services/utils';

describe('Type inheritance', () => {
it('Will pass fields from parent class', () => {
class Base {
@Field() baseField: string;
}

@InterfaceType()
class Foo extends Base {}

const { baseField } = compileInterfaceType(Foo).getFields();

expect(baseField.type).toEqual(GraphQLString);
});

it('Will overwrite fields in child class', () => {
class Base {
@Field() foo: string;
@Field() bar: string;
}

@InterfaceType()
class Foo extends Base {
@Field({ isNullable: false })
foo: string;
}

const { foo, bar } = compileInterfaceType(Foo).getFields();

expect(bar.type).toEqual(GraphQLString);
expect(foo.type).toEqual(new GraphQLNonNull(GraphQLString));
});

it('picks up all the properties even for long chain of extended classes', async () => {
@InterfaceType()
class Vehicle {
@Field() passengers: string;
}

@InterfaceType()
class Car extends Vehicle {
@Field() doorCount: number;
}

@InterfaceType()
class Lamborghini extends Car {
@Field() speed: string;
}
const compiled = compileInterfaceType(Lamborghini);

const fields = compiled.getFields();

expect(fields).toHaveProperty('passengers');
expect(fields).toHaveProperty('doorCount');
expect(fields).toHaveProperty('speed');
expect(getClassWithAllParentClasses(Lamborghini).length).toBe(3);
});
});
Loading