Skip to content

Commit

Permalink
feat: new API
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

Introduce the new graph, model, field, relation, page API and deprecate
the older GraphResolver, ModelResolver, etc. functions.

Add allowAllFields, allowAllFilters graph and model-level options.

Resolves #9, resolves #11, resolves #12.
  • Loading branch information
IlyaSemenov committed May 7, 2023
1 parent 24ad11e commit 79c8d62
Show file tree
Hide file tree
Showing 55 changed files with 3,328 additions and 500 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
"simple-import-sort/exports": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
ignorePatterns: ["/dist/"],
Expand Down
222 changes: 116 additions & 106 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { startStandaloneServer } from "@apollo/server/standalone"
import gql from "graphql-tag"
import Knex from "knex"
import { Model } from "objection"
import { GraphResolver, ModelResolver } from "objection-graphql-resolver"
import * as r from "objection-graphql-resolver"

// Define Objection.js models

Expand Down Expand Up @@ -61,8 +61,8 @@ const typeDefs = gql`

// Map GraphQL types to model resolvers

const resolveGraph = GraphResolver({
Post: ModelResolver(PostModel),
const graph = r.graph({
Post: r.model(PostModel),
})

// Define resolvers
Expand All @@ -71,12 +71,12 @@ const resolvers: ApolloServerOptions<any>["resolvers"] = {
Mutation: {
async create_post(_parent, args, ctx, info) {
const post = await PostModel.query().insert(args)
return resolveGraph(ctx, info, post.$query())
return graph.resolve(ctx, info, post.$query())
},
},
Query: {
posts(_parent, _args, ctx, info) {
return resolveGraph(ctx, info, PostModel.query().orderBy("id"))
return graph.resolve(ctx, info, PostModel.query().orderBy("id"))
},
},
}
Expand Down Expand Up @@ -152,26 +152,18 @@ Relations will be fetched automatically using `withGraphFetched()` when resolvin
Example:

```ts
const resolveGraph = GraphResolver({
User: ModelResolver(UserModel, {
fields: {
id: true,
name: true,
// will use withGraphFetched("posts")
// and process subquery with Post model resolver defined below
posts: true,
},
}),
// No resolver options = access to all fields
Post: ModelResolver(PostModel),
const graph = r.graph({
User: r.model(UserModel),
Post: r.model(PostModel),
})
```

```graphql
```gql
query posts_with_author {
posts {
id
text
# will use withGraphFetched("author") if requested
author {
name
}
Expand All @@ -181,6 +173,7 @@ query posts_with_author {
query user_with_posts {
user(id: ID!) {
name
# will use withGraphFetched("posts") if requested
posts {
id
text
Expand All @@ -191,24 +184,40 @@ query user_with_posts {

[More details and examples for relations.](docs/relations.md)

## Fields access

Access to individual fields can be limited:

```ts
const graph = r.graph({
User: r.model(UserModel, {
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](#api) section below.

## Pagination

Root queries and -to-many nested relations can be paginated.

Example:

```ts
const resolveGraph = GraphResolver({
User: ModelResolver(UserModel, {
const graph = r.graph({
User: r.model(UserModel, {
fields: {
id: true,
name: true,
posts: RelationResolver({
paginate: CursorPaginage({ take: 10, fields: ["-id"] }),
}),
// user.posts will be a page with nodes and continuation cursor
posts: r.page(r.cursor({ fields: ["-id"], take: 10 })),
},
}),
Post: ModelResolver(PostModel),
Post: r.model(PostModel),
})
```

Expand All @@ -218,10 +227,12 @@ To paginate root query, use:
const resolvers = {
Query: {
posts: async (parent, args, ctx, info) => {
const page = await resolveGraph(ctx, info, Post.query(), {
paginate: CursorPaginator({ take: 10, fields: ["-id"] }),
})
return page
return graph.resolvePage(
ctx,
info,
Post.query(),
r.cursor({ take: 10, fields: ["-id"] })
)
},
},
}
Expand All @@ -233,47 +244,32 @@ const resolvers = {

Both root and nested queries can be filtered with GraphQL arguments:

```graphql
```gql
query {
posts(filter: { date: "2020-10-01", author_id__in: [123, 456] }) {
nodes {
id
date
text
author {
id
text
name
}
cursor
}
}
```

Filters will run against database fields, or call model modifiers.

To enable filters, use:

```ts
const resolveGraph = GraphResolver({
Post: ModelResolver(PostModel, {
// enable all filters for all fields
filter: true,
// TODO: granular access
filter: {
date: true,
author_id: true,
published: true, // where published is a model modifier
},
}),
})
```

[More details and examples for filters.](docs/filters.md)

## Virtual attributes

Virtual attributes (getters on models) can be accessed the same way as database fields:

```ts
export class PostModel extends Model {
declare id: number
declare title: string
class PostModel extends Model {
declare id?: number
declare title?: string

get url() {
assert(this.id)
Expand All @@ -282,7 +278,7 @@ export class PostModel extends Model {
}
```

```graphql
```gql
query {
posts {
id
Expand All @@ -296,31 +292,19 @@ query {

## API

The following functions are exported:

```ts
import {
GraphResolver,
ModuleResolver,
FieldResolver,
RelationResolver,
CursorPaginator,
} from "objection-graphql-resolver"
```

### Arguments reference
import * as r from "objection-graphql-resolver"

```ts
const resolveGraph = GraphResolver(
const graph = r.graph(
// Map GraphQL types to model resolvers (required)
{
Post: ModelResolver(
// Required: Objection.js model class
Post: r.model(
// Objection.js model class (required)
PostModel,
// Default: { fields: true }
// Model options
{
// List fields that can be accessed via GraphQL,
// or true = all fields can be accessed
// List fields that can be accessed via GraphQL
// if not provided, all fields can be accessed
fields: {
// Select field from database
id: true,
Expand All @@ -333,18 +317,18 @@ const resolveGraph = GraphResolver(
preview: (query) =>
query.select(raw("substr(text,1,100) as preview")),
// Same as text: true
text: FieldResolver(),
text: r.field(),
// Custom field resolver
text2: FieldResolver({
text2: r.field({
// Model (database) field, if different from GraphQL field
modelField: "text",
}),
preview2: FieldResolver({
preview2: r.field({
// Modify query
select: (query) =>
modify: (query) =>
query.select(raw("substr(text,1,100) as preview2")),
// Post-process selected value
clean(
transform(
// Selected value
preview,
// Current instance
Expand All @@ -361,67 +345,93 @@ const resolveGraph = GraphResolver(
}),
// Select all objects in -to-many relation
comments: true,
comments_page: RelationResolver({
// Select all objects in -to-many relation
all_comments: r.relation({
// Model field, if different from GraphQL field
modelField: "comments",
// Paginate subquery in -to-many relation
paginate: CursorPaginator(
// Enable filters on -to-many relation
filters: true,
// Modify subquery
modify: (query, { liked }) =>
query.where({ liked }).orderBy("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: { take: 10, fields: ["id"] }
// Default: { fields: ["id"], take: 10 }
{
// How many object to take per page
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,
}
),
// Enable filters on -to-many relation
filters: true,
// Modify subquery
modifier: (query, { args }) =>
query.where(args).orderBy("id", "desc"),
// Post-process selected value, see FieldResolver
// clean: ...,
}),
{
// All r.relation() options, such as:
modelField: "comments",
}
),
},
// Modify all queries to this model
modifier: (query, { args }) => query.where(args).orderBy("id", "desc"),
modify: (query, { args }) => query.where(args).orderBy("id", "desc"),
// Allow all fields (`fields` will be used for overrides)
allowAllFields: true,
// Allow filters in all relations
allowAllFilters: true,
}
),
},
// Options (default: empty)
// Graph options
{
// Callback: convert RequestContext into query context
// Default: merge RequestContext into query context as is
// Callback: transform RequestContext before merging with query context
context(ctx) {
return { userId: ctx.passport.user.id }
},
// Allow all fields in all models (`fields` will be used for overrides)
allowAllFields: true,
// Allow filters in all models' relations
allowAllFilters: true,
}
)

const resolvers = {
Query: {
posts: async (parent, args, context, info) => {
const page = await resolveGraph(
posts: (parent, args, ctx, info) => {
return graph.resolve(
// Resolver context (required)
// Will be merged into query context,
// possibly converted with GraphResolver's options.context callback
context,
// possibly converted with graph resolver's options.context callback
ctx,
// GraphQLResolveInfo object, as passed by GraphQL executor (required)
info,
// Root query (required)
Post.query(),
// Default: empty
// Options
{
// Paginator (only works for list queries)
// Default: resolve list query as is
paginate: CursorPaginator({ take: 10, fields: ["-id"] }),
// Enable filters
filters: true,
}
)
return page
},
posts_page: (parent, args, ctx, info) => {
return graph.resolvePage(
// See graph.resolve
ctx,
info,
// Root query (required)
Post.query(),
// Paginator (required)
r.cursor({ fields: ["-id"], take: 10 })
// Options - see graph.resolve
{
filters: true,
}
)
},
},
}
Expand Down
Loading

0 comments on commit 79c8d62

Please sign in to comment.