Skip to content

Commit

Permalink
refactor: separate RelationResolver and FieldResolver
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

FieldResolver no more accepts `filter` and `paginate`, use RelationResolver for relations.
  • Loading branch information
IlyaSemenov committed Jan 16, 2021
1 parent 0e353c7 commit 8d74f04
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 123 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const resolveGraph = GraphResolver({
fields: {
id: true,
name: true,
posts: FieldResolver({
posts: RelationResolver({
paginate: CursorPaginage({ take: 10, fields: ["-id"] }),
}),
},
Expand Down Expand Up @@ -253,6 +253,7 @@ import {
GraphResolver,
ModuleResolver,
FieldResolver,
RelationResolver,
CursorPaginator,
} from "objection-graphql-resolver"
```
Expand Down Expand Up @@ -285,7 +286,7 @@ const resolveGraph = GraphResolver(
text: FieldResolver(),
// Custom field resolver
text2: FieldResolver({
// Source database field
// Model (database) field, if different from GraphQL field
modelField: "text",
}),
preview2: FieldResolver({
Expand All @@ -310,7 +311,9 @@ const resolveGraph = GraphResolver(
}),
// Select all objects in one-to-many relation
comments: true,
comments: FieldResolver({
comments_page: RelationResolver({
// Model field, if different from GraphQL field
modelField: "comments",
// Paginate subquery in one-to-many relation
paginate: CursorPaginator(
// Pagination options
Expand All @@ -325,6 +328,10 @@ const resolveGraph = GraphResolver(
),
// Enable filters on one-to-many relation
filters: true,
// Modify subquery
modifier: (query) => query.orderBy("id", "desc"),
// Post-process selected value, see FIeldResolver
// clean: ...,
}),
},
// Modify all queries to this model
Expand Down
4 changes: 2 additions & 2 deletions docs/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ const resolveGraph = GraphResolver({
name: true,
// If it were posts: true, all posts will be returned.
// Instead, return a page of posts
posts: FieldResolver({
posts: RelationResolver({
paginate: CursorPaginage({ take: 10, fields: ["-id"] }),
}),
// Should you want this, it's still possible to pull all posts (non-paginated)
// under a different GraphQL field
all_posts: FieldResolver({ modelField: "posts" }),
all_posts: RelationResolver({ modelField: "posts" }),
},
}),
Post: ModelResolver(PostModel, {
Expand Down
6 changes: 4 additions & 2 deletions docs/relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ const resolveGraph = GraphResolver({
fields: {
id: true,
name: true,
// will use withGraphFetched("posts")
// and process subquery with Post model resolver defined below
// Use withGraphFetched("posts")
// and process subquery with Post model resolver defined below.
//
// See RelationResolver API for advanced options.
posts: true,
},
}),
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { FieldResolver } from "./resolver/field"
export { ModelResolver } from "./resolver/model"
export { GraphResolver } from "./resolver/graph"
export { ModelResolver } from "./resolver/model"
export { RelationResolver } from "./resolver/relation"
export { CursorPaginator } from "./paginators/cursor"
41 changes: 12 additions & 29 deletions src/resolver/field.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,38 @@
import { Model, QueryBuilder } from "objection"
import { Model, ref } from "objection"

import { FiltersDef } from "../filter"
import { async_run_after } from "../helpers/run_after"
import { PaginatorFn } from "../paginators"
import { AnyContext } from "./graph"
import { ModelFieldResolverFn } from "./model"
import { FieldResolverFn } from "./model"

export interface FieldResolverOptions<M extends Model> {
modelField?: string
select?: FieldResolverFn<M>
filter?: FiltersDef
paginate?: PaginatorFn<M>
clean?(value: any, instance: M, context: AnyContext): any
}

export type FieldResolverFn<M extends Model> = (
query: QueryBuilder<M, any>,
field: string,
resolve_model_field: ModelFieldResolverFn<M>,
) => void

export function FieldResolver<M extends Model>(
options?: FieldResolverOptions<M>,
): FieldResolverFn<M> {
const field_options: FieldResolverOptions<M> = { ...options }

const clean_field = field_options.clean
const select = options?.select
const clean = options?.clean
const model_field = options?.modelField

return function resolve(query, field, resolve_model_field) {
if (field_options.select) {
field_options.select(query, field, resolve_model_field)
return function resolve(query, resolve_opts) {
const { field } = resolve_opts
if (select) {
select(query, resolve_opts)
} else {
resolve_model_field({
model_field: field_options.modelField || field,
filter: field_options.filter,
paginate: field_options.paginate,
})
query.select(model_field ? ref(model_field).as(field) : field)
}
if (clean_field) {
if (clean) {
const context = query.context()
query.runAfter(
async_run_after(async (instance: any) => {
if (!instance) {
// Supposedly, single query builder returning NULL
return instance
}
instance[field] = await clean_field(
instance[field],
instance,
context,
)
instance[field] = await clean(instance[field], instance, context)
return instance
}),
)
Expand Down
118 changes: 38 additions & 80 deletions src/resolver/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,42 @@ import {
ModelClass,
ModelConstructor,
QueryBuilder,
ref,
RelationMappings,
} from "objection"

import { FiltersDef } from "../filter"
import { run_after } from "../helpers/run_after"
import { PaginatorFn } from "../paginators"
import { FieldResolver, FieldResolverFn } from "./field"
import { FieldResolver } from "./field"
import { ResolveTreeFn } from "./graph"
import { RelationResolver } from "./relation"

export type Modifier<M extends Model> = (qb: QueryBuilder<M, any>) => void

export interface ModelResolverOptions<M extends Model> {
modifier?: Modifier<M>
fields?: Record<string, SimpleFieldResolver<M>> | true
}

export type Modifier<M extends Model> = (qb: QueryBuilder<M, any>) => void

export type SimpleFieldResolver<M extends Model> =
| true
| string
| FieldResolverFn<M>

export type ModelResolverFn<M extends Model = Model> = (args: {
tree: ResolveTree
type: string
query: QueryBuilder<M, any>
resolve_tree: ResolveTreeFn
}) => void

export type ModelFieldResolverFn<M extends Model> = (args: {
model_field: string
filter?: FiltersDef
paginate?: PaginatorFn<M>
}) => void
export type SimpleFieldResolver<M extends Model> =
| true
| string
| FieldResolverFn<M>

export type FieldResolverFn<M extends Model> = (
query: QueryBuilder<M, any>,
options: {
// GraphQL field
field: string
// For drilling down
tree: ResolveTree
resolve_tree: ResolveTreeFn
},
) => void

export function ModelResolver<M extends Model = Model>(
model_class: ModelConstructor<M>,
Expand All @@ -64,7 +66,19 @@ export function ModelResolver<M extends Model = Model>(
const relations = ThisModel.relationMappings as RelationMappings

// Default field resolver
const resolve_field: FieldResolverFn<M> = FieldResolver()
const get_field_resolver = (
field: string,
modelField?: string,
): FieldResolverFn<M> => {
const model_field_lookup = modelField || field
if (getter_names.has(model_field_lookup)) {
return () => undefined
} else if (relations[model_field_lookup]) {
return RelationResolver<M, any>({ modelField })
} else {
return FieldResolver<M>({ modelField })
}
}

// Per-field resolvers
const field_resolvers: Record<string, FieldResolverFn<M>> | null =
Expand All @@ -77,9 +91,9 @@ export function ModelResolver<M extends Model = Model>(
if (typeof r0 === "function") {
r = r0
} else if (r0 === true) {
r = resolve_field
r = get_field_resolver(field)
} else if (typeof r0 === "string") {
r = FieldResolver({ modelField: r0 })
r = get_field_resolver(field, r0)
} else {
throw new Error(
`Field resolver must be a function, string, or true; found ${r0}`,
Expand All @@ -103,69 +117,13 @@ export function ModelResolver<M extends Model = Model>(

for (const subtree of Object.values(tree.fieldsByTypeName[type])) {
const field = subtree.name
const r = field_resolvers ? field_resolvers[field] : resolve_field
const r = field_resolvers
? field_resolvers[field]
: get_field_resolver(field)
if (!r) {
throw new Error(`No field resolver defined for field ${type}.${field}`)
}

// Base model field resolver
const resolve_model_field: ModelFieldResolverFn<M> = ({
model_field,
filter,
paginate,
}) => {
if (getter_names.has(field)) {
// Do nothing
} else if (relations[model_field]) {
// Nested relation
if (paginate) {
// withGraphFetched will disregard paginator's runAfter callback (which converts object list into cursor and nodes)
// Save it locally and then re-inject
let paginated_results: any
query
.withGraphFetched(`${model_field}(${field}) as ${field}`, {
joinOperation: "leftJoin", // Remove after https://github.com/Vincit/objection.js/issues/1954 is fixed
maxBatchSize: 1,
})
.modifiers({
[field]: (subquery) => {
resolve_tree({
tree: subtree,
query: subquery,
filter,
paginate,
})
subquery.runAfter((results) => {
// Save paginated results
paginated_results = results
})
},
})
query.runAfter(
// Re-inject paginated results
// They have been overwritten by objection.js by now
run_after((instance) => {
instance[field] = paginated_results
return instance
}),
)
} else {
query
.withGraphFetched(`${model_field}(${field}) as ${field}`)
.modifiers({
[field]: (subquery) =>
resolve_tree({ tree: subtree, query: subquery, filter }),
})
}
} else {
// Normal field - select() it
query.select(
field === model_field ? field : ref(model_field).as(field),
)
}
}

r(query, field, resolve_model_field)
r(query, { field, tree: subtree, resolve_tree })
}

if (
Expand Down
71 changes: 71 additions & 0 deletions src/resolver/relation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Model } from "objection"

import { FiltersDef } from "../filter"
import { run_after } from "../helpers/run_after"
import { PaginatorFn } from "../paginators"
import { FieldResolver, FieldResolverOptions } from "./field"
import { Modifier } from "./model"

export interface RelationResolverOptions<M extends Model, R extends Model>
extends Exclude<FieldResolverOptions<M>, "select"> {
filter?: FiltersDef
paginate?: PaginatorFn<R>
modifier?: Modifier<R>
}

export function RelationResolver<M extends Model, R extends Model>(
options?: RelationResolverOptions<M, R>,
) {
const filter = options?.filter
const paginate = options?.paginate
const modifier = options?.modifier

return FieldResolver<M>({
select(query, { field, tree, resolve_tree }) {
// withGraphFetched will disregard paginator's runAfter callback (which converts object list into cursor and nodes)
// Save it locally and then re-inject
let paginated_results: any

query
.withGraphFetched(
`${options?.modelField || field}(${field}) as ${field}`,
paginate
? {
joinOperation: "leftJoin", // Remove after https://github.com/Vincit/objection.js/issues/1954 is fixed
maxBatchSize: 1,
}
: undefined,
)
.modifiers({
[field]: (subquery) => {
resolve_tree({
tree,
query: subquery,
filter,
paginate,
})
if (paginate) {
subquery.runAfter((results) => {
// Save paginated results
paginated_results = results
})
}
if (modifier) {
modifier(subquery)
}
},
})

if (paginate) {
query.runAfter(
// Re-inject paginated results
// They have been overwritten by objection.js by now
run_after((instance) => {
instance[field] = paginated_results
return instance
}),
)
}
},
})
}
Loading

0 comments on commit 8d74f04

Please sign in to comment.