Node.js GraphQL Framework for building APIs with strong conventions through auto-generated code. With Warthog, set up your data models and resolvers, and it does the rest.
Summary
Warthog is a Node.js GraphQL API framework for quickly building consistent GraphQL APIs that have sorting, filtering and pagination out of the box. It is written in TypeScript and makes heavy use of decorators for concise, declarative code.
Note: Upgrading from 1.0 to 2.0
Warthog is now on version 2.0! There were a few breaking changes that you should consider while upgrading. Also, we tried to keep all new features development on v1, but did end up adding JSON filtering directly to 2.0 as it was much easier given some foundation refactors.
Expand for Breaking change details
More specific scalars
A few fields have been updated to use more specific GraphQL scalars:
ID fields: previously these were represented by type String. Dates now use type ID
Date fields: previously these were represented by type String. Dates now use type DateTime
Since your GraphQL schema has changed and so have the associated TypeScript types in classes.ts, there might be changes in your server code and even perhaps some associated client code if you use these generated classes in your client code.
mockDBConnection has been removed
The old codegen pipeline used TypeORM's metadata in order to generate the GraphQL schema since Warthog didn't also capture this metadata. Warthog now captures the necessary metadata, so we no longer need to lean on TypeORM and therefore we don't need the mockDBConnection we previously used during codegen. Searching your codebase for mockDBConnection and WARTHOG_MOCK_DATABASE/MOCK_DATABASE should do it. If you've been using the Warthog CLI for codegen, you shouldn't have anything to do here.
Project Dependencies Updated
Staying on the latest versions of libraries is good for security, performance and new features. We've bumped to the latest stable versions of each of Warthog's dependencies. This might require some changes to your package.json.
Troubleshooting
Cannot get connection "default" from the connection manager
If you get an error like:
Cannot get connection "default" from the connection manager. Make sure you have created such connection. Also make sure you have called useContainer(Container) in your application before you established a connection and importing any entity.
It could be caused by 2 things:
Remove explicit Container injection
In V1 of Warthog, the README suggested that you should explicitly create your DI containers and pass them into your App instance like so:
In V2, it is recommended that you no longer do this unless you explicitly need access to the Container.
Remove references to Warthog's dependencies
It can sometimes cause problems to explicitly require Warthog's depdendencies (ie type-graphql, typedi, typeorm and typeorm-typedi-extensions). In future versions, remove these explicit dependencies from package.json:
This library is intentionally opinionated and generates as much code as possible. When teams build products quickly, even if they have strong conventions and good linters, the GraphQL can quickly become inconsistent, making it difficult for clients to consume the APIs in a reusable way.
To do this, Warthog automatically generates the following:
See the warthog-starter project for how to use Docker to run Postgres.
Getting Started
Warthog comes with a CLI that makes it easy to get started.
Create new project with the CLI
To install in an existing project, you'll need to create several files in place and then you'll need to call a few Warthog CLI commands that:
Generate a new resource
Create a database
Create a DB migration and run it
Run the server
The following code will get you bootstrapped. You should read through this before running:
# Add warthog so that we can use the CLI
yarn add warthog
# Bootstrap a new application using Warthog CLI
yarn warthog new
# Install dependencies from generated package.json
yarn
# Generate a resource (model, resolver and service)
yarn warthog generate user name! nickname age:int! verified:bool!# Generate typescript classes and GraphQL schema
yarn warthog codegen
# Create your DB
yarn warthog db:create
# Generate the DB migration for your newly generated model
yarn warthog db:migrate:generate --name=create-user-table
# Run the DB migration
yarn warthog db:migrate
# Start the server
yarn start:dev
Here's what this looks like in action:
This will open up GraphQL Playground, where you can execute queries and mutations against your API.
First, add a user by entering the following in the window:
This has a simple example in place to get you started. There are also a bunch of examples in the folder for more advanced use cases.
Note that the examples in the examples folder use relative import paths to call into Warthog. In your projects, you won't need to set this config value as it's only set to deal with the fact that it's using the Warthog core files without consuming the package from NPM. In your projects, you can omit this as I do in warthog-starter.
Warthog Constructs Explained
Models
A model represents both a GraphQL type and a DB table. Warthog exposes a BaseModel class that provides the following columns for free: id, createdAt, createdById, updatedAt, updatedById, deletedAt, deletedById, version. If you use BaseModel in conjunction with BaseService (see below), all of these columns will be updated as you'd expect. The Warthog server will find all models that match the following glob - '/**/*.model.ts'. Ex: user.model.ts
Custom TypeORM and TypeGraphQL options may be passed into the Model decorator using the following signature.
A Warthog resolver exposes queries (reading data) and mutations (writing data). They interact with the DB through services (described below) and typically make use of a bunch of auto-generated TypeScript types in the generated folder for things like sorting and filtering. Warthog will find all resolvers that match the following glob - '/**/*.resolver.ts'. Ex: user.resolver.ts
Services
Services are the glue between resolvers and models. Warthog exposes a class called BaseService that exposes the following methods: find, findOne, create, update, delete. For the find operator, it also maps the auto-generated WhereInput attributes to the appropriate TypeORM Query Builders. Warthog's convention is to name services <model-name>.service.ts. Ex: user.service.ts
Generated Folder
When you start your server, there will be a new generated folder that Warthog creates automatically. The folder contains:
classes.ts: Warthog auto-generates this file from the metadata it collects (from decorators like Model, Query, Resolver, StringField, etc...). Resolvers will import items from here instead of having to manually create them.
schema.graphql: This is auto-generated from our resolvers, models and classes.ts above. Check out this example's schema.graphql to show the additional GraphQL schema Warthog autogenerates.
binding.ts - a graphql-binding for type-safe programmatic access to your API (making real API calls)
Server API (appOptions)
Most of the config in Warthog is done via environment variables (see Config - Environment Variables below). However, more complex/dynamic objects should be passed via the server config.
attribute
description
default
container
TypeDI container. Warthog uses dependency injection under the hood.
empty container
authChecker
An instance of an AuthChecker to secure your resolvers.
context
Context getter of form (request: Request) => Promise<object>
Should we enable subscriptions and open a websocket port
false
WARTHOG_VALIDATE_RESOLVERS
TypeGraphQL validation enabled?
false
Field/Column Decorators
All of the auto-generation magic comes from the decorators added to the attributes on your models. Warthog decorators are convenient wrappers around TypeORM decorators (to create DB schema) and TypeGraphQL (to create GraphQL schema). You can find a list of decorators available in the src/decorators folder. Most of these are also used in the examples folder in this project.
Transactions
There are a few ways to handle transactions in the framework, depending if you want to use BaseService or use your repositories directly.
Using BaseService
To wrap BaseService operations in a transaction, you do 3 things:
Create a function decorated with the @Transaction method decorator
Inject @TransactionManager as a function parameter
Pass the @TransactionManager into calls to BaseService
@Transaction method decorator
The @Transaction decorator opens up a new transaction that is then available via the @TransactionManager. It will automatically close the transaction when the function returns, so it is important to await your service calls and not return a promise in this function.
@Transaction()asynccreateTwoItems(){// ...}
@TransactionManager decorator
The @TransactionManager is essentially the same as a TypeORM EntityManger, except it wraps everything inside of it's transaction.
@Service('UserService')exportclassUserServiceextendsBaseService<User>{constructor(@InjectRepository(User)protectedreadonlyrepository: Repository<User>){super(User,repository);}// GOOD: successful transaction
@Transaction()asyncsuccessfulTransaction(data: DeepPartial<User>,userId: string,
@TransactionManager()manager?: EntityManager): Promise<User[]>{returnPromise.all([this.create(data,userId,{ manager }),this.create(data,userId,{ manager })]);}// GOOD: successful rollback when something errors
@Transaction()asyncfailedTransaction(data: DeepPartial<User>,userId: string,
@TransactionManager()manager?: EntityManager): Promise<User[]>{constinvalidUserData={};constusers=awaitPromise.all([this.create(data,userId,{ manager }),this.create(invalidUserData,userId,{ manager })// This one fails]);returnusers;}// BAD: you can't return a promise here. The function will return and the first// user will be saved even though the 2nd one fails
@Transaction()asyncfailedTransaction(data: DeepPartial<User>,userId: string,
@TransactionManager()manager?: EntityManager): Promise<User[]>{returnawaitPromise.all([this.create(data,userId,{ manager }),this.create(invalidUserData,userId,{ manager })]);}}
Warthog makes building simple CRUD endpoints incredibly easy. In addition, since it is built on top of TypeORM and TypeGraphQL it is flexible enough to handle complex use cases as well. If you need a field to be exposed to either the DB or API, but not both, do the following:
DB-only
If you need to add a column to the DB that does not need to be exposed via the API, you should pass the dbOnly option to your decorator:
请发表评论