Skip to content
This repository was archived by the owner on May 21, 2021. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ APPLICATION_NAME="TypeGraphQL Relay"

# server
SERVER_HOSTNAME=127.0.0.1
SERVER_PORT=8080
SERVER_PORT=4000

# graphql
GRAPHQL_PATH="/graphql"
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules

.env
.env.local
.env.*.local
schema.gql
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v14.15.1
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Calmon Ribeiro

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
postgres:
image: postgres:13-alpine
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
- ${DATABASE_PORT}:5432
environment:
- POSTGRES_USER=${DATABASE_USERNAME}
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
Expand Down
11 changes: 11 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
collectCoverage: false,
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
coverageReporters: ['html'],
preset: 'ts-jest',
roots: ['<rootDir>/tests'],
setupFiles: ['dotenv-flow/config'],
setupFilesAfterEnv: ['jest-extended'],
testEnvironment: 'node',
testMatch: ['**/functional/**/*.test.ts', '**/unit/**/*.test.ts']
}
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
"private": true,
"name": "typegraphql-relay",
"description": "Relay-compliant GraphQL server using TypeGraphQL.",
"engineStrict": true,
"engines": {
"node": ">=14.15.1"
},
"scripts": {
"lint": "eslint */**/*.{js,ts} --quiet --fix",
"start:dev": "tsnd -r dotenv-defaults/config --respawn --transpile-only src"
"start:dev": "NODE_ENV=development tsnd -r dotenv-flow/config --respawn --transpile-only src",
"test:watch": "jest --runInBand --watch",
"docker:up:dev": "docker-compose --env-file .env.development.local -p gql-development up -d",
"docker:up:test": "docker-compose --env-file .env.test.local -p gql-test up -d"
},
"repository": {
"type": "git",
Expand All @@ -16,7 +23,7 @@
"relay"
],
"author": "Calmon Ribeiro <calmonrib@gmail.com>",
"license": "UNLICENSED",
"license": "MIT",
"bugs": {
"url": "https://github.com/calmonr/typegraphql-relay/issues"
},
Expand All @@ -25,7 +32,7 @@
"apollo-server-express": "^2.19.0",
"class-validator": "^0.12.2",
"consola": "^2.15.0",
"dotenv-defaults": "^2.0.1",
"dotenv-flow": "^3.2.0",
"express": "^4.17.1",
"graphql": "^15.4.0",
"graphql-relay": "^0.6.0",
Expand All @@ -39,15 +46,19 @@
"devDependencies": {
"@types/express": "^4.17.8",
"@types/graphql-relay": "^0.6.0",
"@types/jest": "^26.0.18",
"@types/node": "^14.14.6",
"@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1",
"eslint": "^7.12.1",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.6.3",
"jest-extended": "^0.11.5",
"lint-staged": "^10.5.1",
"prettier": "^2.1.2",
"ts-jest": "^26.4.4",
"ts-node-dev": "^1.0.0",
"typescript": "^4.0.5"
}
Expand Down
5 changes: 4 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

## Introduction

*This documentation will be completely rewritten explaining better how to get started. This means that this repository is constantly changing and that is why we don't recommend using it in production until all features have been completed.*

This is a GraphQL server boilerplate that follows the [Global Object Identification](https://graphql.org/learn/global-object-identification/), [GraphQL Server Specification](https://relay.dev/docs/en/graphql-server-specification.html) and [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) specifications.

## Running
Expand Down Expand Up @@ -48,11 +50,12 @@ Congratulations, the server is running. 🚀

The most important parts are implemented and working properly but we have room for improvements.

- [x] Tests
- [ ] Ordering
- [ ] Filtering
- [ ] Custom arguments example
- [ ] Error handling
- [ ] DataLoader
- [ ] Production ready

Feel free to send suggestions and pull requests.

Expand Down
31 changes: 17 additions & 14 deletions src/loaders/database.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useContainer
} from 'typeorm'

import { ProductEntity } from '../modules/product/product.entity'
import { isDevelopment } from '../utils'

const {
DATABASE_HOST,
Expand All @@ -16,20 +16,23 @@ const {
DATABASE_NAME
} = process.env

const options: ConnectionOptions = {
host: DATABASE_HOST,
port: DATABASE_PORT,
username: DATABASE_USERNAME,
password: DATABASE_PASSWORD,
database: DATABASE_NAME,
type: 'postgres',
synchronize: true,
logging: true,
entities: [ProductEntity]
}
export default (options?: Partial<ConnectionOptions>): Promise<Connection> => {
const defined: ConnectionOptions = {
host: DATABASE_HOST,
port: DATABASE_PORT,
username: DATABASE_USERNAME,
password: DATABASE_PASSWORD,
database: DATABASE_NAME,
type: 'postgres',
synchronize: isDevelopment,
logging: isDevelopment,
entities: [`${__dirname}/../modules/**/*.entity.{ts,js}`]
}

export default (): Promise<Connection> => {
useContainer(Container)

return createConnection(options)
return createConnection({
...defined,
...options
} as ConnectionOptions)
}
27 changes: 21 additions & 6 deletions src/loaders/gql.loader.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { ApolloServer } from 'apollo-server-express'
import { Express } from 'express'
import { buildSchema } from 'type-graphql'
import { GraphQLSchema } from 'graphql'
import { buildSchema, BuildSchemaOptions } from 'type-graphql'
import Container from 'typedi'

import { NodeResolver } from '../relay/node.resolver'
import { isDevelopment } from '../utils'

const { GRAPHQL_PATH } = process.env

export default async (app: Express): Promise<void> => {
const schema = await buildSchema({
resolvers: [`${__dirname}/../modules/**/*.resolver.{ts,js}`],
export const createSchema = (
options?: Partial<BuildSchemaOptions>
): Promise<GraphQLSchema> => {
const defined = {
resolvers: [NodeResolver, `${__dirname}/../modules/**/*.resolver.{ts,js}`],
container: Container,
emitSchemaFile: true
})
emitSchemaFile: isDevelopment
}

return buildSchema({
...defined,
...options
} as BuildSchemaOptions)
}

export default async (app: Express): Promise<void> => {
const schema = await createSchema()

const apolloServer = new ApolloServer({
schema
Expand Down
6 changes: 3 additions & 3 deletions src/modules/product/payloads/add-product.payload.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Field, ObjectType } from 'type-graphql'

import { ProductEntity } from '../product.entity'
import { Product } from '../product.entity'

@ObjectType()
export class AddProductPayload {
@Field(() => ProductEntity, { nullable: true })
readonly product?: ProductEntity
@Field(() => Product, { nullable: true })
readonly product?: Product
}
4 changes: 2 additions & 2 deletions src/modules/product/product.edge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EdgeType } from '../../decorators/relay/edge.type'
import { ProductEntity } from './product.entity'
import { Product } from './product.entity'

@EdgeType(ProductEntity)
@EdgeType(Product)
export class ProductEdge {}
8 changes: 4 additions & 4 deletions src/modules/product/product.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, ID, ObjectType } from 'type-graphql'
import { Field, ObjectType } from 'type-graphql'
import {
Column,
CreateDateColumn,
Expand All @@ -7,11 +7,11 @@ import {
UpdateDateColumn
} from 'typeorm'

import { NodeInterface } from '../relay/node.interface'
import { Node } from '../../relay/node.interface'

@Entity('products')
@ObjectType('Product', { implements: NodeInterface })
export class ProductEntity extends NodeInterface {
@ObjectType({ implements: Node })
export class Product extends Node {
@PrimaryGeneratedColumn()
readonly id!: number

Expand Down
6 changes: 4 additions & 2 deletions src/modules/product/product.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Inject } from 'typedi'
import { AddProductInput } from './inputs/product.input'
import { ProductService } from './product.service'
import { ProductConnection } from './product.connection'
import { ConnectionArgs } from '../../relay/connection.args'
import { ConnectionArguments } from '../../relay/connection.args'
import { AddProductPayload } from './payloads/add-product.payload'

@Resolver()
Expand All @@ -13,7 +13,9 @@ export class ProductResolver {
private service!: ProductService

@Query(() => ProductConnection)
async products(@Args() args: ConnectionArgs): Promise<ProductConnection> {
async products(
@Args() args: ConnectionArguments
): Promise<ProductConnection> {
return this.service.paginate(args)
}

Expand Down
12 changes: 6 additions & 6 deletions src/modules/product/product.service.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { Service } from 'typedi'
import { Repository } from 'typeorm'
import { InjectRepository } from 'typeorm-typedi-extensions'
import { ConnectionArguments } from 'graphql-relay'

import { ProductEntity } from './product.entity'
import { Product } from './product.entity'
import { AddProductInput } from './inputs/product.input'
import { ConnectionArguments } from 'graphql-relay'
import { ProductConnection } from './product.connection'
import { connectionFromRepository } from '../../relay/connection.factory'

@Service()
export class ProductService {
@InjectRepository(ProductEntity)
private readonly repository!: Repository<ProductEntity>
@InjectRepository(Product)
private readonly repository!: Repository<Product>

async paginate(args: ConnectionArguments): Promise<ProductConnection> {
return connectionFromRepository(args, this.repository)
}

async findById(id: string): Promise<ProductEntity | undefined> {
async findById(id: string): Promise<Product | undefined> {
return this.repository.findOne(id)
}

async add({ name, description }: AddProductInput): Promise<ProductEntity> {
async add({ name, description }: AddProductInput): Promise<Product> {
return this.repository.save({ name, description })
}
}
7 changes: 5 additions & 2 deletions src/relay/connection.args.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Max, Min } from 'class-validator'
import { ConnectionArguments, ConnectionCursor } from 'graphql-relay'
import {
ConnectionArguments as RelayConnectionArguments,
ConnectionCursor
} from 'graphql-relay'
import { ArgsType, Field, Int } from 'type-graphql'

import { CannotWith } from '../validators/cannot-with.validator'

// TODO: validate must provide at least first or last
@ArgsType()
export class ConnectionArgs implements ConnectionArguments {
export class ConnectionArguments implements RelayConnectionArguments {
@Field(() => String, {
nullable: true,
description:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Field, ID, InterfaceType } from 'type-graphql'

@InterfaceType('Node', { description: 'An object with a global ID.' })
export abstract class NodeInterface {
@InterfaceType({ description: 'An object with a global ID.' })
export abstract class Node {
@Field(() => ID, {
name: 'id',
description: 'The global ID of the object.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
} from 'type-graphql'
import { Inject } from 'typedi'

import { ProductService } from '../product/product.service'
import { NodeInterface } from './node.interface'
import { ProductService } from '../modules/product/product.service'
import { Node } from './node.interface'

@Resolver(() => NodeInterface)
@Resolver(() => Node)
export class NodeResolver {
@Inject()
private readonly productService!: ProductService
Expand All @@ -29,7 +29,7 @@ export class NodeResolver {

// TODO: use dataloader
// TODO: find a better way to automate and avoid if conditions
@Query(() => NodeInterface, {
@Query(() => Node, {
nullable: true,
description: 'Fetches an object given its global ID.'
})
Expand All @@ -38,7 +38,7 @@ export class NodeResolver {
description: 'The global ID of the object.'
})
globalId: string
): Promise<NodeInterface | undefined> {
): Promise<Node | undefined> {
const { type, id } = fromGlobalId(globalId)

if (type == 'Product') {
Expand All @@ -50,7 +50,7 @@ export class NodeResolver {
)
}

@Query(() => [NodeInterface], {
@Query(() => [Node], {
nullable: 'items',
description: 'Fetches objects given their global IDs.'
})
Expand Down
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { NODE_ENV } = process.env

export const isDevelopment = NODE_ENV === 'development'
Empty file.
Loading