Compose a great GraphQL apps on the fly with TypeScript and power of TypeDoc. No more hell of decorators and pain with unreadable the GraphQLXXXType spaghetti!
- Writing the GraphQL schemas as a class tree.
- Providing Service Container and Context State to the resolvers.
- Defining independent GraphQL schemas nearby in one project.
- Defining GraphQL types on the fly by an interface definition.
See packages/test to meet the GraphQL schema definition example.
First you need install @graphly/cli
and @graphly/types
yarn add @graphly/cli @graphly/types
or npm i @graphly/cli @graphly/types
Next write your schema
MySchema/MySchema.ts
import {Schema} from "@graphly/type";
export class MySchema extends Schema {
public readonly query: MyQuery;
/**
* This method is required and uses to detect a schema path.
*/
public static getSchemaLocation() {
return __filename;
}
}
MySchema/MyQuery.ts
import {ObjectType} from "@graphly/type";
export class MyQuery extends ObjectType {
public hello() {
return "Hello, World";
}
}
MySchema/MyContext.ts
import {Context} from "@graphly/type";
import {MyContainer} from "./MyContainer";
export class MyContext extends Context<MyContainer, {}, {}> {}
MySchema/MyContainer.ts
import {Container} from "@graphly/type";
export class MyContainer extends Container<{}> {}
Let's build a typemap
of this schema graphly-cli compose MySchema/MySchema.ts
it will write MySchema.json
nearby compiled MySchema.js.
Then you can use Scope
from @graphly/type
to define your own scope for this schema
and call graphql
import {graphql} from "graphql";
import {Scope} from "@graphly/type";
import {MySchema} from "./MySchema/MySchema";
import {MyContext} from "./MySchema/MyContext";
import {MyContainer} from "./MySchema/MyContainer";
async function main() {
const myScope = new Scope({
schema: MySchema,
context: MyContext,
container: MyContainer,
config: {},
});
const state = {};
const config = await myScope.createConfig(state);
console.log(await graphql(
config.schema,
`query MyQuery {hello}`,
config.rootValue,
config.context,
));
}
Typical structure of a project may be like this
MySchema
MySchema/MySchema.ts
MySchema/MyQuery.ts
MySchema/MyMutation.ts
MySchema/Query/TodoQuery.ts
MySchema/Query/Todo.ts
MySchema/Query/Todo/TodoStatus.ts
MySchema/Mutation/TodoMutation.ts
MySchema/Input/TodoInput.ts
MySchema/MyContainer.ts
MySchema/MyContext.ts
...
Remember that a project directory should contain only schema
, enum
, context
or container
types.
Resolvers are class methods which return only the output type. You can pass only the input/service type as a resolver argument
class TotoMutation extends ObjectType {
public add(todo: TodoInput, context: MyContext): Returns<Todo> {
return context.todos.add(todo);
}
}
Sometimes TypeScript can resolve an incorrect return type like this:
class MyQuery {
public async session(context: MyContext) { // Promise<UserSession> | Promise<undefined>
if (context.isAuthorized) {
return context.userSession;
}
return undefined;
}
}
To fix this behavior just force a return type for the resolver
class MyQuery {
public async session(context: MyContext): ReturnsNullable<UserSession> {
// ...
}
}
Force use Returns
or Promise
for non-nullable and ReturnsNullable
for nullable (for null
and undefined
type too) in async resolvers.
class MeQuery {
public async me(): Returns<User> {
// ...
}
public async bestFriend(): ReturnsNullable<User> {
// ...
}
}
Context and Service Container are service types. Context is a request state and Container is a global state and service provider like a database link and so on.
You should linking context with container to getting relevant type in its context
interface IConfig {
dsn: string;
}
interface IState {
user: User;
}
class MyContext extends Context<MyContainer, IConfig, IState> {
public get currentUser() {
return this.state.user;
}
public get todos() {
return this.container.repository.collection("todos");
}
}
class MyContainer extends Container<IConfig> {
public get repository() {
return createDbConnection(this.config.dsn);
}
}
The service type can be used as a resolver argument
class TodoMutation extends ObjectType {
public async add(todo: TodoInput, context: MyContext): Returns<Todo> {
const {todos} = context;
const {insertedId} = await todos.insertOne(todo);
return todos.findOne({_id: insertedId});
}
}
Remember, that container
will be passed to context
and resolver arguments
with resolved properties. It touches container
getters too.
You can use interfaces to generating typically structures on the fly. For example you can use pagination interface to auto-generate its type
import {ObjectType, IObject, TypeInt, Returns} from "@graphly/type";
class TodoQuery extends ObjectType {
public async search(offset: TypeInt = 0, limit: TypeInt = 10, context: MyContext): Returns<IPageable<Todo>> {
const count = await context.todos.countDocuments();
const node = await context.todos.find()
.offset(offset)
.limit(limit)
.toArray();
return {count, limit, offset, node}
}
}
interface IPageable<T extends ObjectType> extends IObject {
readonly count: TypeInt;
readonly offset: TypeInt;
readonly limit: TypeInt;
readonly node: T[];
}
In this case TodoQuery.search
will define a new type:
type TodoQuerySearch {
offset: Int!
limit: Int!
count: Int!
node: [Todo!]!
}
import {Scope} from "@graphly/type";
import {MySchema} from "./MySchema";
import {MyContext} from "./MyContext";
import {MyContainer} from "./MyContainer";
import {config} from "./config";
async function main() {
const myScope = new Scope({
schema: MySchema,
context: MyContext,
container: MyContainer,
config,
});
const {schema, context, rootValue} = await myScope.createServerConfig({
validateRequest,
validateAuthorization,
createSessionState(container, payload) {
// check authorization and return context state
return state;
}
});
const server = new ApolloServer({
schema,
rootValue,
context: ({req}) => context(req),
});
// ... run apollo server
}
It's a little bit difficult to parse all types of the TypeScript reflection so class schema has some limitations for syntax that can be used:
- Don't use spread syntax in resolver arguments.
- Use Promise, AsyncIterator, Subscription and IObject only as a parents of return types.
- Don't use nearby resolvers.
- Be careful to use initial values of class members.
Be notice that it's an experimental library, unstable for production and may be changed its api in future.
MIT