Skip to content

Commit

Permalink
fix: select fields with ref()
Browse files Browse the repository at this point in the history
Prevent naming clash on fields in joins.
Automatically select pagination key fields.
Fix #7
  • Loading branch information
IlyaSemenov committed Apr 16, 2022
1 parent 55192c1 commit 46a2d29
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 15 deletions.
10 changes: 6 additions & 4 deletions src/paginators/cursor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, QueryBuilder, raw } from "objection"
import { Model, QueryBuilder, raw, ref } from "objection"

import { PaginatorFn } from "."

Expand Down Expand Up @@ -84,9 +84,11 @@ export function CursorPaginator<M extends Model>(

// Set query order
query.clearOrder()
fields.forEach((field) =>
query.orderBy(field.name, field.desc ? "desc" : "asc")
)
fields.forEach((field) => {
const f = ref(field.name).from(query.tableRef())
// TODO: prevent potential name clash with aliases like .as(`_${table_ref}_order_key_0`)
query.select(f).orderBy(f, field.desc ? "desc" : "asc")
})

if (cursor) {
set_query_cursor(query, cursor)
Expand Down
6 changes: 5 additions & 1 deletion src/resolver/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export function FieldResolver<M extends Model>(
if (select) {
select(query, resolve_opts)
} else {
query.select(model_field ? ref(model_field).as(field) : field)
query.select(
ref(model_field || field)
.from(query.tableRef())
.as(field)
)
}
if (clean) {
const context = query.context()
Expand Down
19 changes: 10 additions & 9 deletions src/resolver/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ModelClass,
ModelConstructor,
QueryBuilder,
ref,
RelationMappings,
} from "objection"

Expand Down Expand Up @@ -127,16 +128,16 @@ export function ModelResolver<M extends Model = Model>(
r(query, { field, tree: subtree, resolve_tree })
}

if (
!query.has(
((op: any) =>
op.name === "select" && op.args[0] === ThisModel.idColumn) as any
// Performance: select ID if nothing was selected.
// This avoid automatic "select *" generated by objection.
if (!query.has((QueryBuilder as any).SelectSelector)) {
query.select(
ref(
typeof ThisModel.idColumn === "string"
? ThisModel.idColumn
: ThisModel.idColumn[0]
).from(query.tableRef())
)
) {
// Always select ID:
// 1. This is useful for potential $query()
// 2. This avoid automatic "select *" when not a single normal field was selected
query.select(ThisModel.idColumn)
}

const clean_instance = model_options.clean
Expand Down
8 changes: 8 additions & 0 deletions src/shim.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import "objection"

declare module "objection" {
interface QueryBuilder<M extends Model, R = M[]> {
// Not sure why it's missing in official typings
tableRef(): string
}
}
83 changes: 83 additions & 0 deletions tests/auto-select-pagination-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import gql from "graphql-tag"
import { Model } from "objection"
import {
CursorPaginator,
GraphResolver,
ModelResolver,
} from "objection-graphql-resolver"
import tap from "tap"

import { Resolvers, setup } from "./setup"

class BookModel extends Model {
static tableName = "book"

id?: number
title?: string
author?: string
}

const schema = gql`
type Book {
id: Int!
title: String!
author: String!
}
type BookPage {
nodes: [Book!]!
cursor: String
}
type Query {
library: BookPage!
}
`

const resolve_graph = GraphResolver({
Book: ModelResolver(BookModel),
})

const resolvers: Resolvers = {
Query: {
library: (_parent, _args, ctx, info) =>
resolve_graph(ctx, info, BookModel.query(), {
paginate: CursorPaginator({ take: 1, fields: ["author", "id"] }),
}),
},
}

tap.test("auto select pagination key", async (tap) => {
const { client, knex } = await setup(tap, { typeDefs: schema, resolvers })

await knex.schema.createTable("book", (book) => {
book.increments("id").notNullable().primary()
book.string("title").notNullable()
book.string("author").notNullable()
})

await BookModel.query().insertGraph([
{ title: "1984", author: "George Orwell" },
{ title: "Tom Sawyer", author: "Mark Twain" },
])

tap.strictSame(
await client.request(
gql`
{
library {
nodes {
title
}
}
}
`
),
{
library: {
nodes: [{ title: "1984" }],
},
},
"pagination works without selecting author explicitly"
)
})
2 changes: 2 additions & 0 deletions tests/issue-1.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Regression test for https://github.com/IlyaSemenov/objection-graphql-resolver/issues/1

import gql from "graphql-tag"
import { Model } from "objection"
import { GraphResolver, ModelResolver } from "objection-graphql-resolver"
Expand Down
4 changes: 3 additions & 1 deletion tests/issue-7.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Regression test for https://github.com/IlyaSemenov/objection-graphql-resolver/issues/7

import gql from "graphql-tag"
import { Model } from "objection"
import { GraphResolver, ModelResolver } from "objection-graphql-resolver"
Expand Down Expand Up @@ -85,7 +87,7 @@ const resolvers: Resolvers = {
},
}

tap.todo("m2m with extra columns in relation table", async (tap) => {
tap.test("m2m: naming clash with column in relation table", async (tap) => {
const { client, knex } = await setup(tap, { typeDefs: schema, resolvers })

await knex.schema.createTable("author", (author) => {
Expand Down
63 changes: 63 additions & 0 deletions tests/select-only-virtual-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import gql from "graphql-tag"
import { Model } from "objection"
import { GraphResolver, ModelResolver } from "objection-graphql-resolver"
import tap from "tap"

import { Resolvers, setup } from "./setup"

class BookModel extends Model {
static tableName = "book"

id?: number

get foo() {
return "whatever"
}
}

const schema = gql`
type Book {
id: Int!
foo: String!
}
type Query {
library: [Book!]!
}
`

const resolve_graph = GraphResolver({
Book: ModelResolver(BookModel),
})

const resolvers: Resolvers = {
Query: {
library: (_parent, _args, ctx, info) =>
resolve_graph(ctx, info, BookModel.query()),
},
}

tap.test("select virtual attribute only", async (tap) => {
const { client, knex } = await setup(tap, { typeDefs: schema, resolvers })

await knex.schema.createTable("book", (book) => {
book.integer("id").notNullable().primary()
})

await BookModel.query().insert({ id: 1 })

tap.strictSame(
await client.request(
gql`
{
library {
foo
}
}
`
),
{
library: [{ foo: "whatever" }],
}
)
})

0 comments on commit 46a2d29

Please sign in to comment.