objection-fetch-graphql
A helper library to resolve GraphQL queries directly with objection.js models/relations.
- Effective: selects only requested fields and relations (using fine-tuned
withGraphFetched
) - Unlimited nested resolvers (traversing
relationMappings
) - Virtual attributes
- Dynamic filters like
{ date: "2020-10-01", category__in: ["News", "Politics"] }
- Hook into subqueries with query modifiers
Install
yarn add objection-fetch-graphql
Use
Create GraphQL schema:
type Post {
id: ID
title: String
text: String
}
type Query {
posts: [Post!]!
}
Create objection.js model:
import { Model } from "objection"
export class PostModel extends Model {
static tableName = "posts"
}
Import objection-fetch-graphql
somewhere in entry point:
// Somewhere in entry point: it monkey-patches objection.js
import "objection-fetch-graphql"
Define resolver:
export const resolvers = {
Query: {
posts: (parent, args, ctx, info) => {
return PostModel.query().withGraphQL(info)
},
},
}
Run GraphQL server:
new ApolloServer({ typeDefs, resolvers }).listen({ port: 4000 })
Define GraphQL query:
query get_all_posts {
posts {
id
title
# text is not requested, and will not be selected from DB
}
}
Execute it:
// Using @graphql-codegen/typescript-graphql-request
const sdk = getSdk(new GraphQLClient("http://127.0.0.1:4000"))
await sdk.get_all_posts()
Relations
Relations will be fetched automatically using withGraphFetched()
for the nested fields.
Consider schema:
type Post {
id: ID
text: String
author: User
}
Model:
export class PostModel extends Model {
static tableName = "posts"
static get relationMappings() {
return {
author: {
relation: Model.BelongsToOneRelation,
modelClass: UserModel,
join: { from: "posts.author_id", to: "users.id" },
},
}
}
}
Query:
query posts_with_author {
posts {
id
text
author {
name
}
}
}
Resolver:
// for the query above, will pull posts with related author object
PostModel.query().withGraphQL(info)
Filters
Queries can be filtered like this:
PostModel.query().withGraphQL(info, {
filter: {
date: "2020-10-01",
// Only pull posts where author_id is 123 or 456.
author_id__in: [123, 456],
},
})
which adds WHERE date='2020-10-01' AND author.id IN (123, 456)
.
The suggested workflow is using a dedicated untyped GraphQL query arg to pass filters:
scalar Filter
type Query {
posts(filter: Filter): [Post!]!
}
and then in resolver:
export const resolvers = {
Query: {
posts: (parent, { filter }, ctx, info) => {
return PostModel.query().withGraphQL(info, { filter })
},
},
}
Supported operators:
exact
in
- TODO:
lt
,gt
,lte
,gte
,like
,ilike
,contains
,icontains
Filtering nested relations
You can filter nested relations with a nested filter:
UserModel.query().withGraphQL(info, {
filter: {
id: 123,
posts: {
date: "2020-10-01",
},
},
})
Note that it only works reasonably for one-to-many relations, as in the example above.
For instance, filtering posts with { author: { name: "John" } }
will not work as expected.
Filtering with model modifiers
If you define modifiers on a model class:
export class PostModel extends Model {
static modifiers = {
public: (query) => query.whereNull("delete_time"),
search: (query, term) => query.where("text", "ilike", `%${term}%`),
}
}
then you can filter results with:
UserModel.query().withGraphQL(info, {
filter: {
public: true, // even though the actual value is ignored, sending true is a reasonable convention
search: "hello",
},
})
Modifier filters take precedence over raw field filters.
Query model modifiers
All models can be filtered using query-level modifiers:
PostModel.query().withGraphQL(info, {
modifiers: {
User: (query) => query.where("active", true),
},
})
Virtual attributes
Virtual attributes (getters on models) can be accessed as usual:
export class PostModel extends Model {
get url() {
return `/${this.id}.html`
}
}
query get_all_posts {
posts {
id
title
url
}
}
Virtual attribute dependencies
If a getter relies on certain model fields (such as if url
needs title
), you will need to select all of them in the query.
Alternatively, you can setup getter dependencies with select.${field}
modifier, like this:
export class PostModel extends Model {
static modifiers = {
"graphql.select.url": (query) => query.select(ref("title")),
}
get url() {
assert(this.title !== undefined)
return `/${urlencode(this.title)}-${this.id}.html`
}
}
Virtual attributes provided by the database
select.${field}
modifier can also be used to fill the virtual attribute with a raw subquery:
type Post {
id: ID
title: String
upper_title: String
}
export class PostModel extends Model {
static modifiers = {
"graphql.select.upper_title": (query) =>
query.select(raw("upper(title) as upper_title")),
}
// Optionally for Typescript
declare readonly upper_title: string
}
Global model modifiers
The following model modifiers, when exist, are automatically applied on each query (including when resolving nested relations):
export class PostModel extends Model {
static modifiers = {
// Applied on each query
graphql: (query) => query.where("is_hidden", false),
// Applied on each query that is returning an array (not a single object)
"graphql.many": (query) => query.orderBy("publish_time", "desc"),
// Applied on each top-level query (not nested relation)
"graphql.top": (query) => query.where("is_top", true),
}
}