Skip to content
Permalink
Browse files

feat(api): fix API for public consumption (#30)

* feat(api): fix API for public consumption
  • Loading branch information...
goldcaddy77 committed Jan 20, 2019
1 parent f2759fa commit ae0ae2f0a59b38fc2dc3bad6d03a423f02574ee8
Showing with 554 additions and 304 deletions.
  1. +1 −3 .markdownlint.json
  2. +67 −16 README.md
  3. +2 −2 examples/1-simple-model/src/index.ts
  4. 0 examples/1-simple-model/src/{user.entity.ts → user.model.ts}
  5. +9 −6 examples/1-simple-model/src/user.resolver.ts
  6. +1 −1 examples/1-simple-model/tsconfig.json
  7. +20 −0 examples/1-simple-model/tslint.json
  8. +4 −1 examples/2-complex-example/package.json
  9. +25 −10 examples/2-complex-example/src/app.ts
  10. +6 −6 examples/2-complex-example/src/index.test.ts
  11. +4 −2 examples/2-complex-example/src/index.ts
  12. 0 examples/2-complex-example/src/modules/user/{user.entity.ts → user.model.ts}
  13. +10 −12 examples/2-complex-example/src/modules/user/user.resolver.ts
  14. +6 −5 examples/2-complex-example/tools/seed.ts
  15. +1 −1 examples/2-complex-example/tsconfig.json
  16. +20 −0 examples/2-complex-example/tslint.json
  17. +54 −3 examples/2-complex-example/yarn.lock
  18. +1 −1 examples/3-one-to-many-relationship/src/{post.entity.ts → post.model.ts}
  19. +11 −8 examples/3-one-to-many-relationship/src/post.resolver.ts
  20. +1 −1 examples/3-one-to-many-relationship/src/{user.entity.ts → user.model.ts}
  21. +12 −9 examples/3-one-to-many-relationship/src/user.resolver.ts
  22. +20 −0 examples/3-one-to-many-relationship/tslint.json
  23. +1 −1 examples/4-many-to-many-relationship/src/join-with-metadata/{role.entity.ts → role.model.ts}
  24. +4 −4 examples/4-many-to-many-relationship/src/join-with-metadata/role.resolver.ts
  25. +2 −2 ...les/4-many-to-many-relationship/src/join-with-metadata/{user-role.entity.ts → user-role.model.ts}
  26. +10 −12 examples/4-many-to-many-relationship/src/join-with-metadata/user-role.resolver.ts
  27. +1 −1 examples/4-many-to-many-relationship/src/join-with-metadata/{user.entity.ts → user.model.ts}
  28. +6 −8 examples/4-many-to-many-relationship/src/join-with-metadata/user.resolver.ts
  29. +1 −1 examples/4-many-to-many-relationship/src/simple-join-table/{author.entity.ts → author.model.ts}
  30. +6 −8 examples/4-many-to-many-relationship/src/simple-join-table/author.resolver.ts
  31. +1 −1 examples/4-many-to-many-relationship/src/simple-join-table/{post.entity.ts → post.model.ts}
  32. +6 −8 examples/4-many-to-many-relationship/src/simple-join-table/post.resolver.ts
  33. +20 −0 examples/4-many-to-many-relationship/tslint.json
  34. +14 −2 package.json
  35. +3 −3 src/core/Context.ts
  36. +84 −64 src/core/app.ts
  37. +1 −2 src/core/index.ts
  38. +17 −1 src/core/logger.ts
  39. +3 −3 src/core/types.ts
  40. +1 −1 src/decorators/EnumField.test.ts
  41. +1 −1 src/decorators/EnumField.ts
  42. +2 −2 src/decorators/Model.ts
  43. +11 −11 src/{core → gql}/binding.ts
  44. +1 −0 src/gql/index.ts
  45. +4 −4 src/metadata/metadata-storage.ts
  46. +4 −4 src/middleware/DataLoaderMiddleware.ts
  47. +3 −3 src/middleware/ErrorMiddleware.ts
  48. +23 −25 src/schema/SchemaGenerator.ts
  49. +11 −11 src/schema/TypeORMConverter.ts
  50. +3 −3 src/schema/getSchemaInfo.ts
  51. +4 −5 src/tgql/BaseResolver.ts
  52. +2 −2 src/tgql/authChecker.ts
  53. +9 −7 src/torm/EverythingSubscriber.ts
  54. +6 −6 src/torm/createConnection.ts
  55. 0 src/torm/ormconfig.ts
  56. +0 −6 src/utils/EntityCreator.ts
  57. +1 −1 src/utils/index.ts
  58. +6 −0 src/utils/object-to-model.ts
  59. +1 −3 tslint.json
  60. +6 −1 yarn.lock
@@ -2,9 +2,7 @@
"MD009": {
"br_spaces": 2
},
"MD013": {
"code_blocks": false
},
"MD013": false,
"MD026": {
"punctuation": ".,;:"
},
@@ -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:
@@ -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

@@ -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';
@@ -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 {
@@ -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

@@ -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

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

MIT © Dan Caddigan


@@ -1,5 +1,5 @@
import 'reflect-metadata';
import * as dotenv from 'dotenv';
import 'reflect-metadata';
import { Container } from 'typedi';

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

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

bootstrap().catch((error: Error) => {
File renamed without changes.
@@ -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
@@ -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);
@@ -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);
}
}
@@ -20,5 +20,5 @@
"types": ["jest", "isomorphic-fetch", "node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules/**/*"]
"exclude": ["node_modules/**/*", "generated/**/*"]
}
@@ -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/*"]
}
}
@@ -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",
@@ -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"
@@ -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": {
@@ -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: 'admin@test.com',
id: 'abc12345',
permissions: ['user:read', 'user:update', 'user:create', 'user:delete', 'photo:delete']
}
};
},
dbOptions
);
warthogImportPath: '../../../src' // Path written in generated classes
});
}
@@ -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;

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

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

@@ -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();
@@ -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();
@@ -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);
});

0 comments on commit ae0ae2f

Please sign in to comment.
You can’t perform that action at this time.