Skip to content

Commit

Permalink
feat(api): fix API for public consumption (#30)
Browse files Browse the repository at this point in the history
* feat(api): fix API for public consumption
  • Loading branch information
goldcaddy77 authored Jan 20, 2019
1 parent f2759fa commit ae0ae2f
Show file tree
Hide file tree
Showing 60 changed files with 554 additions and 304 deletions.
4 changes: 1 addition & 3 deletions .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
"MD009": {
"br_spaces": 2
},
"MD013": {
"code_blocks": false
},
"MD013": false,
"MD026": {
"punctuation": ".,;:"
},
Expand Down
83 changes: 67 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Warthog - Auto-generated GraphQL APIs
# Warthog - GraphQL API Framework

[![npm version](https://img.shields.io/npm/v/warthog.svg)](https://www.npmjs.org/package/warthog)
[![CircleCI](https://circleci.com/gh/goldcaddy77/warthog/tree/master.svg?style=shield)](https://circleci.com/gh/goldcaddy77/warthog/tree/master)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](#badge)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Join the chat at https://gitter.im/warthog-graphql/community](https://badges.gitter.im/warthog-graphql/community.svg)](https://gitter.im/warthog-graphql/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

Opinionated framework for building GraphQL APIs with strong conventions. With
Warthog, set up your data models and the following are automatically generated:
Opinionated framework for building GraphQL APIs with strong conventions. With Warthog, set up your data models and the following are automatically generated:

- Database schema - generated by [TypeORM](https://github.com/typeorm/typeorm)
- Your entire GraphQL Schema including:
Expand All @@ -20,8 +19,7 @@ Warthog, set up your data models and the following are automatically generated:

## Warning

The API for this library is very much a moving target. It will likely shift
until version 2, at which time it will become stable.
The API for this library is still subject to change. It will likely shift until version 2, at which time it will become stable. I'd love early adopters, but please know that there might be some breaking changes for a few more weeks.

## Install

Expand All @@ -31,11 +29,11 @@ yarn add warthog

## Usage

Check out the [examples folder](https://github.com/goldcaddy77/warthog/tree/v1/examples)
to see how to use Warthog in a project or check out the
[warthog example](https://github.com/goldcaddy77/warthog-example) repo.
Check out the [warthog-example](https://github.com/goldcaddy77/warthog-example) repo to see how to use Warthog in a project. There are also a bunch of examples in the [examples](./examples/README.md) folder. Note that these use relative import paths to call into Warthog instead of pulling from NPM.

Create an entity:
### 1. Create a Model

The model will auto-generate your database table and graphql types. Warthog will find all models that match the following glob - `'/**/*.model.ts'`. So for this file, you would name it `user.model.ts`

```typescript
import { BaseModel, Model, StringField } from 'warthog';
Expand All @@ -47,7 +45,50 @@ export class User extends BaseModel {
}
```

Now, when you start your server, the following will be generated:
### 2. Create a Resolver

The resolver auto-generates queries and mutations in your GraphQL schema. Warthog will find all resolvers that match the following glob - `'/**/*.resolver.ts'`. So for this file, you would name it `user.resolver.ts`

```typescript
import { User } from './user.model';

@Resolver(User)
export class UserResolver extends BaseResolver<User> {
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {
super(User, userRepository);
}

@Query(returns => [User])
async users(
@Args() { where, orderBy, limit, offset }: UserWhereArgs
): Promise<User[]> {
return this.find<UserWhereInput>(where, orderBy, limit, offset);
}

@Mutation(returns => User)
async createUser(@Arg('data') data: UserCreateInput, @Ctx() ctx: BaseContext): Promise<User> {
return this.create(data, ctx.user.id);
}
}
```

### 3. Run your server

```typescript

import 'reflect-metadata';
import { Container } from 'typedi';
import { App } from 'warthog';

async function bootstrap() {
const app = new App({ container: Container });
return app.start();
}

bootstrap()
```

When you start your server, there will be a new `generated` folder that has your GraphQL schema in `schema.graphql`. This contains:

```graphql
type User implements BaseGraphQLObject {
Expand Down Expand Up @@ -115,10 +156,22 @@ input UserWhereUniqueInput {
}
```

## Limitations
Notice how we've only added a single field on the model and you get pagination, filtering and tracking of who created, updated and deleted records automatically.

## Config

Since Warthog relies heavily on conventions, it only supports postgres currently
for DBs.
| value | ENV var | option name | default |
| --- | --- | --- | --- |
| host | APP_HOST | appOptions.host | no default |
| app port | APP_PORT | appOptions.port | 4000 |
| generated folder | _none_ | appOptions.generatedFolder | _current-dir_ + `generated` |

## Intentionally Opinionated

Warthog is intentionally opinionated

- Database - currently only supports Postgres. This could be easily changed, but I don't have the need currently
- Soft deletes - no records are ever deleted, only "soft deleted". The base service used in resolvers filters out the deleted records by default.

## Thanks

Expand All @@ -128,7 +181,7 @@ Special thanks to the authors of:
- [TypeGraphQL](https://github.com/19majkel94/type-graphql)
- [Prisma](https://github.com/prisma/prisma)

Ultimately, Warthog is a really opinionated, yet flexible composition of these libraries
Warthog is essentially a really opinionated composition of TypeORM and TypeGraphQL that uses similar GraphQL conventions to the Prisma project.

## Contribute

Expand All @@ -137,5 +190,3 @@ PRs accepted, fire away! Or add issues if you have use cases Warthog doesn't co
## License

MIT © Dan Caddigan


4 changes: 2 additions & 2 deletions examples/1-simple-model/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'reflect-metadata';
import * as dotenv from 'dotenv';
import 'reflect-metadata';
import { Container } from 'typedi';

dotenv.config();
Expand All @@ -12,7 +12,7 @@ async function bootstrap() {
warthogImportPath: '../../../src' // Path written in generated classes
});

await app.start();
return app.start();
}

bootstrap().catch((error: Error) => {
Expand Down
File renamed without changes.
15 changes: 9 additions & 6 deletions examples/1-simple-model/src/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { Arg, Args, Ctx, Mutation, Query, Resolver } from 'type-graphql';
import { Repository } from 'typeorm';
import { InjectRepository } from 'typeorm-typedi-extensions';

import { BaseResolver, Context, StandardDeleteResponse } from '../../../src';
import { BaseContext, BaseResolver, StandardDeleteResponse } from '../../../src';
import { UserCreateInput, UserUpdateArgs, UserWhereArgs, UserWhereInput, UserWhereUniqueInput } from '../generated';

import { User } from './user.entity';
import { User } from './user.model';

// Note: we have to specify `User` here instead of (of => User) because for some reason this
// changes the object reference when it's trying to add the FieldResolver and things break
Expand All @@ -19,7 +19,7 @@ export class UserResolver extends BaseResolver<User> {
@Query(returns => [User])
async users(
@Args() { where, orderBy, limit, offset }: UserWhereArgs,
@Ctx() ctx: Context,
@Ctx() ctx: BaseContext,
info: GraphQLResolveInfo
): Promise<User[]> {
return this.find<UserWhereInput>(where, orderBy, limit, offset);
Expand All @@ -31,17 +31,20 @@ export class UserResolver extends BaseResolver<User> {
}

@Mutation(returns => User)
async createUser(@Arg('data') data: UserCreateInput, @Ctx() ctx: Context): Promise<User> {
async createUser(@Arg('data') data: UserCreateInput, @Ctx() ctx: BaseContext): Promise<User> {
return this.create(data, ctx.user.id);
}

@Mutation(returns => User)
async updateUser(@Args() { data, where }: UserUpdateArgs, @Ctx() ctx: Context): Promise<User> {
async updateUser(@Args() { data, where }: UserUpdateArgs, @Ctx() ctx: BaseContext): Promise<User> {
return this.update(data, where, ctx.user.id);
}

@Mutation(returns => StandardDeleteResponse)
async deleteUser(@Arg('where') where: UserWhereUniqueInput, @Ctx() ctx: Context): Promise<StandardDeleteResponse> {
async deleteUser(
@Arg('where') where: UserWhereUniqueInput,
@Ctx() ctx: BaseContext
): Promise<StandardDeleteResponse> {
return this.delete(where, ctx.user.id);
}
}
2 changes: 1 addition & 1 deletion examples/1-simple-model/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"types": ["jest", "isomorphic-fetch", "node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules/**/*"]
"exclude": ["node_modules/**/*", "generated/**/*"]
}
20 changes: 20 additions & 0 deletions examples/1-simple-model/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": ["typestrict", "tslint:latest", "tslint-config-prettier"],
"rules": {
"no-string-throw": false,
"class-name": false,
"interface-over-type-literal": false,
"interface-name": [false],
"max-classes-per-file": false,
"member-access": [false],
"no-submodule-imports": false,
"no-unused-variable": false,
"no-implicit-dependencies": false,
"no-var-keyword": false,
"no-floating-promises": true,
"no-console": false
},
"linterOptions": {
"exclude": ["node_modules/**/*", "generated/*", "src/migration/*"]
}
}
5 changes: 4 additions & 1 deletion examples/2-complex-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"db:create": "createdbjs $(dotenv -p TYPEORM_DATABASE) 2>&1 || :",
"db:drop": "dropdbjs $(dotenv -p TYPEORM_DATABASE) 2>&1 || :",
"db:seed:dev": "dotenv -- ts-node tools/seed.ts",
"lint": "tslint --fix -c ./tslint.json -p ./tsconfig.json",
"playground:open": "open http://localhost:$(dotenv -p APP_PORT)/playground",
"start": "npm-run-all --parallel start:ts playground:open",
"start:debug": "yarn start:ts --inspect",
Expand All @@ -17,6 +18,7 @@
"watch:ts": "nodemon -e ts,graphql -x ts-node --type-check src/index.ts"
},
"dependencies": {
"debug": "^4.1.1",
"pgtools": "^0.3.0",
"reflect-metadata": "^0.1.12",
"typescript": "^3.2.2"
Expand All @@ -33,7 +35,8 @@
"nodemon": "^1.18.9",
"npm-run-all": "^4.1.5",
"ts-jest": "^23.10.5",
"ts-node": "^7.0.1"
"ts-node": "^7.0.1",
"tslint": "^5.12.1"
},
"jest": {
"transform": {
Expand Down
35 changes: 25 additions & 10 deletions examples/2-complex-example/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import 'reflect-metadata';

import { Container } from 'typedi';
import { App, AppOptions } from '../../../src/';

// import { User } from './modules/user/user.entity';
import { App, BaseContext } from '../../../src/';

export function getApp(appOptions: Partial<AppOptions> = {}, dbOptions: any = {}) {
return new App(
{
container: Container,
warthogImportPath: '../../../src', // Path written in generated classes
...appOptions
// import { User } from './modules/user/user.model';

interface Context extends BaseContext {
user: {
email: string;
id: string;
permissions: string;
};
}

export function getApp() {
return new App<Context>({
container: Container,
// Inject a fake user. In a real app you'd parse a JWT to add the user
context: request => {
return {
user: {
email: '[email protected]',
id: 'abc12345',
permissions: ['user:read', 'user:update', 'user:create', 'user:delete', 'photo:delete']
}
};
},
dbOptions
);
warthogImportPath: '../../../src' // Path written in generated classes
});
}
12 changes: 6 additions & 6 deletions examples/2-complex-example/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'reflect-metadata';
import { GraphQLError } from 'graphql';
import 'reflect-metadata';

import { Binding } from '../generated/binding';

import { getApp } from './app';
import { User } from './modules/user/user.entity';
import { User } from './modules/user/user.model';

let app = getApp({}, { logging: false });
const app = getApp({}, { logging: false });
let binding: Binding;
let testUser: User;

Expand All @@ -32,9 +32,9 @@ beforeAll(async done => {
done();
});

afterAll(done => {
afterAll(async done => {
(console.error as any).mockRestore();
app.stop();
await app.stop();
done();
});

Expand All @@ -51,7 +51,7 @@ describe('Users', () => {
});

test('createdAt sort', async done => {
let users = await binding.query.users({ limit: 1, orderBy: 'createdAt_DESC' }, `{ id firstName}`);
const users = await binding.query.users({ limit: 1, orderBy: 'createdAt_DESC' }, `{ id firstName}`);

expect(console.error).not.toHaveBeenCalled();
expect(users).toBeDefined();
Expand Down
6 changes: 4 additions & 2 deletions examples/2-complex-example/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'reflect-metadata';

import * as dotenv from 'dotenv';
dotenv.config();

import { getApp } from './app';

dotenv.config();

async function bootstrap() {
const app = getApp();
await app.start();
Expand All @@ -12,7 +14,7 @@ async function bootstrap() {
bootstrap().catch((error: Error) => {
console.error(error);
if (error.stack) {
console.error(error.stack!.split('\n'));
console.error(error.stack.split('\n'));
}
process.exit(1);
});
Loading

0 comments on commit ae0ae2f

Please sign in to comment.