Skip to content

Commit ebbb96c

Browse files
committed
feat: add dynamic object factories
1 parent 13925b7 commit ebbb96c

9 files changed

+179
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Type Relay [name TBD] [![pipeline status](https://gitlab.com/wemaintain/type-relay/badges/master/pipeline.svg)](https://gitlab.com/wemaintain/type-relay/commits/master)[![coverage report](https://gitlab.com/wemaintain/type-relay/badges/master/coverage.svg)](https://gitlab.com/wemaintain/type-relay/commits/master)
1+
# Type Relay [name TBD] [![pipeline status](https://gitlab.com/wemaintain/type-relay/badges/master/pipeline.svg)](https://gitlab.com/wemaintain/type-relay/commits/master) [![coverage report](https://gitlab.com/wemaintain/type-relay/badges/master/coverage.svg)](https://gitlab.com/wemaintain/type-relay/commits/master)
22

33
Type Relay is a librairy designed to work alongside [TypeGraphQL](https://typegraphql.ml/) and make it easy to paginated your results using the [Relay spec](https://facebook.github.io/relay/graphql/connections.htm)
44

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
describe('RelayedConnection', () => {
2+
it.todo('Should call the DynamicObject factory')
3+
})
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { Container } from 'typedi'
12
import { ReturnTypeFunc, MethodAndPropDecorator } from '../types/types'
3+
import { DynamicObjectFactory } from '../graphql/dynamic-object.factory'
24

35
export function RelayedConnection (type: ReturnTypeFunc): MethodAndPropDecorator
46
export function RelayedConnection (type: ReturnTypeFunc, through: ReturnTypeFunc): MethodAndPropDecorator
57
export function RelayedConnection (type: ReturnTypeFunc, through?: ReturnTypeFunc): MethodAndPropDecorator {
68
return <M>(target: any, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor<M>) => {
7-
89
}
910
}

packages/type-relay/src/decorators/relayed-connection.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { DynamicObjectFactory } from "./dynamic-object.factory";
2+
import { Container } from "typedi";
3+
import { TypeRelayConfig } from "../services/type-relay-config.service";
4+
import * as TGQL from 'type-graphql'
5+
6+
describe('DynamicObject factory', () => {
7+
let dynamicObjectFactory = new DynamicObjectFactory();
8+
9+
beforeEach(() => {
10+
dynamicObjectFactory = new DynamicObjectFactory();
11+
})
12+
13+
describe('makeEdgeConnection', () => {
14+
it('Should throw if PAGINATION_OBJECT wasn\'t init\'d', () => {
15+
expect(() => dynamicObjectFactory.makeEdgeConnection("", () => Object)).toThrowError(/PageInfo/)
16+
})
17+
18+
it('Should throw if PAGINATION_OBJECT isn\'t a function', () => {
19+
Container.set('PAGINATION_OBJECT', {})
20+
expect(() => dynamicObjectFactory.makeEdgeConnection("", () => Object)).toThrowError(/PageInfo/)
21+
})
22+
23+
it('Should throw if PAGINATION_OBJECT returns undefined', () => {
24+
Container.set('PAGINATION_OBJECT', () => undefined)
25+
expect(() => dynamicObjectFactory.makeEdgeConnection("", () => Object)).toThrowError(/PageInfo/)
26+
})
27+
28+
it('Should return a decorated Edge Object', () => {
29+
new TypeRelayConfig({ orm: 'type-orm' });
30+
31+
const fieldSpy = jest.spyOn(TGQL, 'Field');
32+
const objectSpy = jest.spyOn(TGQL, 'ObjectType');
33+
34+
const objectInnerSpy = jest.fn();
35+
const fieldInnerSpy = jest.fn();
36+
fieldSpy.mockImplementation((_a, _b) => {
37+
return fieldInnerSpy;
38+
});
39+
objectSpy.mockImplementation((_a, _b) => {
40+
return objectInnerSpy;
41+
});
42+
43+
const { Edge } = dynamicObjectFactory.makeEdgeConnection("Foo", () => Object)
44+
45+
expect(Edge).toBeTruthy();
46+
expect(fieldInnerSpy.mock.calls).toIncludeAllMembers([
47+
[ Edge.prototype, 'node' ],
48+
[ Edge.prototype, 'cursor' ]
49+
])
50+
51+
expect(objectSpy).toHaveBeenCalledWith('FooEdge');
52+
53+
fieldSpy.mockRestore();
54+
objectSpy.mockRestore();
55+
})
56+
57+
it('Should return a decorated Connection Object', () => {
58+
new TypeRelayConfig({ orm: 'type-orm' });
59+
60+
const fieldSpy = jest.spyOn(TGQL, 'Field');
61+
const objectSpy = jest.spyOn(TGQL, 'ObjectType');
62+
63+
const objectInnerSpy = jest.fn();
64+
const fieldInnerSpy = jest.fn();
65+
fieldSpy.mockImplementation((_a, _b) => {
66+
return fieldInnerSpy;
67+
});
68+
objectSpy.mockImplementation((_a, _b) => {
69+
return objectInnerSpy;
70+
});
71+
72+
const { Connection } = dynamicObjectFactory.makeEdgeConnection("Foo", () => Object)
73+
74+
expect(Connection).toBeTruthy();
75+
76+
expect(objectSpy).toHaveBeenCalledWith('FooConnection');
77+
78+
fieldSpy.mockRestore();
79+
objectSpy.mockRestore();
80+
})
81+
82+
})
83+
})
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
import { Service, Container } from 'typedi'
3+
import { TypeValue } from '../types/types'
4+
import { Field, ObjectType } from 'type-graphql'
5+
import * as Relay from 'graphql-relay'
6+
7+
/**
8+
* Factory service for generating Objects that are dynamic and might be appened multiple times
9+
* to the SDL. Such as Edges and Connections
10+
*/
11+
@Service()
12+
export class DynamicObjectFactory {
13+
/**
14+
* Creates and decorates two new Objects to connect two entities via Relay
15+
* Will return a relay Edge and Connection Object to be used when fetching this specific object
16+
* @param connectionName name of the connection, usually `EntitAEntityB`
17+
* @param nodeType type of the EntityB
18+
* @param edgeAugment if the relation is N:M, "augment" the edge with the join table
19+
*/
20+
public makeEdgeConnection<T extends TypeValue> (
21+
connectionName: string,
22+
nodeType: () => T,
23+
edgeAugment?: () => new () => Object): { Connection: new () => Relay.Connection<T>; Edge: new () => Relay.Edge<T> } {
24+
if (!edgeAugment) edgeAugment = () => Object
25+
const PageInfo = this._getPageInfo()
26+
const Edge = this._makeEdge(connectionName, nodeType, edgeAugment)
27+
const Connection = this._makeConnection(connectionName, Edge, PageInfo)
28+
29+
return { Edge, Connection }
30+
}
31+
32+
/**
33+
* Create the Edge for the given node type & augment
34+
* @param connectionName name of the connection to prefix the edge
35+
* @param nodeType type of the node for this edge
36+
* @param edgeAugment optional type to augment the edge with
37+
*/
38+
protected _makeEdge<T extends TypeValue> (connectionName: string, nodeType: () => T, edgeAugment: () => new () => any): new () => Relay.Edge<T> {
39+
const Edge = class extends edgeAugment().prototype.constructor implements Relay.Edge<T> {
40+
public node!: T;
41+
42+
public cursor!: Relay.ConnectionCursor;
43+
}
44+
45+
Object.assign(Edge, edgeAugment())
46+
47+
Field(nodeType)(Edge.prototype, 'node')
48+
Field(() => String, { description: 'Used in `before` and `after` args' })(Edge.prototype, 'cursor')
49+
ObjectType(`${connectionName}Edge`)(Edge)
50+
51+
return Edge
52+
}
53+
54+
/**
55+
* Create the Connection object for the given Edge
56+
* @param connectionName name of the connection to prefix the object
57+
* @param Edge type of the edges we want to have in this connection
58+
* @param PageInfo type of the PageInfo we're using
59+
*/
60+
protected _makeConnection<T extends TypeValue> (connectionName: string, Edge: new () => Relay.Edge<T>, PageInfo: new () => Relay.PageInfo): new () => Relay.Connection<T> {
61+
@ObjectType(`${connectionName}Connection`)
62+
class Connection implements Relay.Connection<T> {
63+
@Field(() => PageInfo)
64+
public pageInfo!: Relay.PageInfo;
65+
66+
@Field(() => [Edge])
67+
public edges!: Relay.Edge<T>[];
68+
}
69+
70+
return Connection
71+
}
72+
73+
/**
74+
* Get the local PageInfo model
75+
* @throws if PageInfo cannot be accessed.
76+
*/
77+
protected _getPageInfo (): new () => Relay.PageInfo {
78+
try {
79+
const PageInfoFn: () => (new () => Relay.PageInfo) = Container.get('PAGINATION_OBJECT')
80+
const PageInfo = PageInfoFn()
81+
82+
if (!PageInfo) throw new Error()
83+
return PageInfo
84+
} catch (e) {
85+
throw new Error(`Couldn't find PageInfo Object. Did you forget to init TypeRelay?`)
86+
}
87+
}
88+
}
File renamed without changes.

packages/type-relay/src/graphql/shared-object.factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class SharedObjectFactory {
4141
*/
4242
public generateConnectionArgs (prefix: string): new () => Relay.ConnectionArguments {
4343
const argsName = `${prefix}ConnectionArgs`
44-
// This is trick so the local class will be called argsName, as the ArgsType decorator doesn't take
44+
// This is a trick so the local class will be called argsName, as the ArgsType decorator doesn't take
4545
// a name argument and instead uses the class name in the SDL.
4646
const darkMagic = {
4747
[argsName]: class implements Relay.ConnectionArguments {

packages/type-relay/tests/services/type-relay-config.service.test.ts renamed to packages/type-relay/src/services/type-relay-config.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TypeRelayConfig } from './../../src/services/type-relay-config.service'
1+
import { TypeRelayConfig } from './type-relay-config.service'
22
import {Container} from 'typedi'
33
describe('TypeRelayConfig', () => {
44
let typeRelayConfig: TypeRelayConfig | null = null

0 commit comments

Comments
 (0)