This package has been deprecated

Author message:

Renamed to objection-graphql-resolver

objection-fetch-graphql
TypeScript icon, indicating that this package has built-in type declarations

2.2.0 • Public • Published

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),
	}
}

Readme

Keywords

none

Package Sidebar

Install

npm i objection-fetch-graphql

Weekly Downloads

0

Version

2.2.0

License

MIT

Unpacked Size

19.9 kB

Total Files

11

Last publish

Collaborators

  • ilyasemenov