A helper library to resolve GraphQL queries directly with Orchid ORM tables and relations.
- Highly effective: selects only requested fields and relations.
- Unlimited nested resolvers.
- Pagination.
- Filters like
{ date: "2020-10-01", category__in: ["News", "Politics"] }
. - Hook into (sub)queries with query modifiers.
- Hook into field results to restrict access to sensitive information.
Note that there is a sister project: objection-graphql-resolver
which does the same for Objection.js ORM.
This is pre-release and not battle tested. orchid-graphql
is a quick adaptation of objection-graphql-resolver
.
In particular the typings are not ready. The resolver returns a generic Query
most of the time.
npm i orchid-graphql
Run GraphQL server:
// Everything is put into a single file for demonstration purposes.
//
// In real projects, you will want to separate tables, typedefs,
// resolvers, and the server into their own modules.
import { ApolloServer, ApolloServerOptions } from "@apollo/server"
import { startStandaloneServer } from "@apollo/server/standalone"
import gql from "graphql-tag"
import * as r from "orchid-graphql"
import { createBaseTable, orchidORM } from "orchid-orm"
// Define database tables
const BaseTable = createBaseTable()
class PostTable extends BaseTable {
readonly table = "post"
columns = this.setColumns((t) => ({
id: t.identity().primaryKey(),
text: t.text(0, 5000),
}))
}
const db = orchidORM(
{
databaseURL: process.env.DATABASE_URL,
log: true,
},
{
post: PostTable,
},
)
await db.$query`
create table post (
id serial primary key,
text text not null
);
`
// Define GraphQL schema
const typeDefs = gql`
type Post {
id: Int!
text: String!
}
type Mutation {
create_post(text: String!): Post!
}
type Query {
posts: [Post!]!
}
`
// Map GraphQL types to table resolvers
const graph = r.graph({
Post: r.table(db.post),
})
// Define resolvers
const resolvers: ApolloServerOptions<any>["resolvers"] = {
Mutation: {
async create_post(_parent, args, context, info) {
const post = await db.post.create(args)
return await graph.resolve(db.post.find(post.id), { context, info })
},
},
Query: {
async posts(_parent, _args, context, info) {
return await graph.resolve(db.post, { context, info })
},
},
}
// Start GraphQL server
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
})
console.log(`Listening on ${url}`)
Query it with GraphQL client:
import { GraphQLClient } from "graphql-request"
import gql from "graphql-tag"
const client = new GraphQLClient("http://127.0.0.1:4000")
await client.request(
gql`
mutation create_post($text: String!) {
new_post: create_post(text: $text) {
id
}
}
`,
{ text: "Hello, world!" },
)
const { posts } = await client.request(gql`
query {
posts {
id
text
}
}
`)
console.log(posts)
Relations will be fetched automatically when resolving nested fields.
Example:
const graph = r.graph({
User: r.type(db.user),
Post: r.type(db.post),
})
query posts_with_author {
posts {
id
text
# will use subquery if requested
author {
name
}
}
}
query user_with_posts($id: ID!) {
user(id: $id) {
name
# will use subquery if requested
posts {
id
text
}
}
}
More details and examples for relations.
Access to individual fields can be limited:
const graph = r.graph({
User: r.type(db.user, {
fields: {
id: true,
name: true,
// other fields not specified here, such as user password,
// will not be accessible
},
}),
})
This API also allows to fine-tune field selectors, see API section below.
Root queries and -to-many nested relations can be paginated.
const graph = r.graph({
User: r.type(db.user, {
fields: {
id: true,
name: true,
// user.posts will be a page with nodes and continuation cursor
posts: r.page(r.cursor({ fields: ["-id"], take: 10 })),
},
}),
Post: r.type(db.post),
})
To paginate root query, use:
const resolvers = {
Query: {
posts: async (parent, args, context, info) => {
return await graph.resolvePage(
db.post,
r.cursor({ take: 10, fields: ["-id"] }),
{ context, info },
)
},
},
}
More details and examples for pagination.
Both root and nested queries can be filtered with GraphQL arguments:
query {
posts(filter: { date: "2020-10-01", author_id__in: [123, 456] }) {
id
date
text
author {
id
name
}
}
}
Filters will run against database fields, or call field modifiers.
More details and examples for filters.
import * as r from "orchid-graphql"
const graph = r.graph(
// Map GraphQL types to table resolvers (required)
{
Post: r.table(
// orchid-orm bound table (required)
db.post,
// Table resolver options
{
// List fields that can be accessed via GraphQL
// if not provided, all fields can be accessed
fields: {
// Select field from database
id: true,
// Descend into relation
// (related table must be also registered in this graph resolver)
author: true,
// Modify query when this field is resolved
preview: (q) =>
q.select({ preview: q.sql<string>`substr(text,1,100)` }),
// Same as text: true
text: r.field(),
// Custom field resolver
text2: r.field({
// Table field, if different from GraphQL field
tableField: "text",
}),
preview2: r.field({
// Modify query
modify: (q) =>
q.select({ preview2: q.sql<string>`substr(text,1,100)` }),
// Post-process selected value
transform(
// Selected value
preview,
// Current instance
post,
// Field resolve context: graph, tree, type, field, filters, context
context,
) {
if (preview.length < 100) {
return preview
} else {
return preview + "..."
}
},
}),
// Select all objects in -to-many relation
comments: true,
// Select all objects in -to-many relation
all_comments: r.relation({
// Table field, if different from GraphQL field
tableField: "comments",
// Enable filters on -to-many relation
filters: true,
// Modify subquery
modify: (q, { liked }) => q.where({ liked }).order({ id: "DESC" }),
// Post-process selected values, see r.field()
// transform: ...,
}),
// Paginate subquery in -to-many relation
comments_page: r.page(
// Paginator
r.cursor(
// Pagination options
// Default: { fields: ["id"], take: 10 }
{
// Which fields to use for ordering
// Prefix with - for descending sort
fields: ["name", "-id"],
// How many object to take per page
take: 10,
},
),
{
// All r.relation() options, such as:
tableField: "comments",
},
),
},
// Modify all queries to this table
modify: (
// ORM (sub)query
query,
// Table resolve context: graph, tree, type, filters, context
context,
) => query.where(context.tree.args).order({ id: "DESC" }),
// Allow all fields (`fields` will be used for overrides)
allowAllFields: true,
// Allow filters in all relations
allowAllFilters: true,
},
),
},
// Graph options
{
// Allow all fields in all tables (`fields` will be used for overrides)
allowAllFields: true,
// Allow filters in all relations of all tables
allowAllFilters: true,
},
)
const resolvers = {
Query: {
posts: async (parent, args, context, info) => {
return await graph.resolve(
// Root query (required)
db.post,
// Options (required)
{
// Resolver context
context,
// GraphQLResolveInfo object, as passed by GraphQL executor (required)
info,
// Enable filters
filters: true,
},
)
},
posts_page: async (parent, args, context, info) => {
return await graph.resolvePage(
// Root query (required)
db.post,
// Paginator (required)
r.cursor({ fields: ["-id"], take: 10 }),
// Options (required) - see graph.resolve
{
context,
info,
filters: true,
},
)
},
},
}
graph.resolve(db.user.find(1), {
// As comes from Apollo
context,
// As comes from Apollo
info,
// If resolving subfield
path: ["subfield"],
})