graphql-orm
Monorepo for helper libraries to resolve GraphQL queries with ORM tables and relations.
Packages and the respective supported ORMs:
- objection-graphql-resolver — for Objection.js.
- orchid-graphql — for Orchid ORM.
Resolve GraphQL query with Objection.js or Orchid ORM
License: MIT License
Monorepo for helper libraries to resolve GraphQL queries with ORM tables and relations.
Packages and the respective supported ORMs:
Consider a mutation:
type Mutation {
login(input: LoginInput!): LoginResult
}
input LoginInput {
username: String!
password: String!
}
type LoginResult {
token: String!
user: User!
}
With implementation like:
resolvers.mutation("login", async (args, context, info) => {
const { username, password } = args.input
const user_id = await login(username, password)
if (user_id) {
const token = await create_token(user_id)
const user = await graph.resolve(db.user.find(user_id), { context, info }) // <---- here
return { token, user }
} else {
throw new Error("Invalid username or password.")
}
})
This will currently crash with "Resolver not found for type LoginResult."
We need a way to tell graph.resolve()
that we want it to resolve a nested field from info
.
Currently, one can either allowAllFilters
on graph level (which affects all queries), or allowAllFilters
on model level (which affects resolving nested fields of that model).
I would like to allow filters on a certain model itself. Say if enable it for Book model, all queries to books (root queries and when books are a nested field of some other model) should be allowed to be filtered.
Proposed syntax:
export const Book = r.table(db.book, { filters: true })
Currently, all fields are enabled to be queried only if fields
is omitted in ModelResolver options:
const resolveGraph = GraphResolver({
Foo: ModelResolver(FooModel) // can access all fields on Foo
})
customizing one field disables access to all other fields:
const resolveGraph = GraphResolver({
Foo: ModelResolver(FooModel, {
fields: {
bars: RelationResolver({ /* customize */ })
// can't access other fields in Foo anymore unless explicitly defined!
}
})
})
Instead, it should be possible to customize certain field resolvers yet still keep 'allow all' logic:
const resolveGraph = GraphResolver({
Foo: ModelResolver(FooModel, {
allowAllFields: true, // <---- not sure about the naming
fields: {
bars: RelationResolver({ /* customize */ })
// still can access other fields in Foo with default resolvers
}
})
})
I need to clean a model instance to ensure there's no private data leak. I can see that there's a clean callback in FieldResolver, but in my scenario I'd rather prefer to clean the whole model instance afterwards (after all fields have been fetched and cleaned separately). I would expect ModelResolver
to accept clean(instance, context)
in its options alongside fields
and modifier
.
Currently, a resolver only allows to modify a query based on context:
export const Org = r.table<ResolverContext>(db.org, {
fields: {
id: true,
name: true,
operator_id: 'operatorId',
menu_variant: true,
menu_variants: true,
},
modify: (q, { context }) =>
context
.bound_role!.allow_read_org(q as typeof db.org)
.order({ name: 'ASC' }),
})
I would like to have a way to configure everything based on context — primarily, limit the set of allowed fields based on the request context.
Hi, thank you for this great library.
I am having issues compiling.
I narrowed the error below to this piece of code:
import { IdentificationModel } from 'db/models/model.identification'
import { GraphResolver, ModelResolver } from 'objection-graphql-resolver'
export function identityResolver(parent, args, ctx, info) {
const resolveGraph = GraphResolver({
// Map GraphQL types to model resolvers
Identitication: ModelResolver(IdentificationModel, {
// List fields that can be accessed via GraphQL
fields: {
id: true,
identifier: true,
type: true,
expiration: true
}
})
})
const q = IdentificationModel.query()
return resolveGraph(ctx, info, q)
}
If I comment this code, ... I don't get the error below.
This is so weird... any idea?
RangeError: Maximum call stack size exceeded
at recursiveTypeRelatedTo (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:50861:44)
at isRelatedTo (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:50530:34)
at checkTypeRelatedTo (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:50216:26)
at isTypeRelatedTo (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:50179:24)
at isTypeAssignableTo (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:49470:20)
at getSimplifiedConditionalType (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:48225:47)
at getSimplifiedType (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:48161:41)
at getNormalizedType (/Users/orefalo/.local/share/nvm/v14.17.0/lib/node_modules/typescript/lib/tsc.js:50192:57)
Currently, a model/table modifier runs for all queries against that model:
In many cases, such as when using the modifier for sorting or limiting data access, running it for -to-one relations is useless.
I propose to add new r.model({ manyModifier })
(not sure about the naming) that will only run for root query and -to-many relations.
hey Ilya,
Been experimenting and everything looks to be working well although I’m having problems retrieving graphql data via Model. ManyToManyRelatation relationships.
The actual query generated seems to include extra fields that aren’t prefixed correctly and as such won’t execute.
Do you know if these kinds of relationships work currently? When I try making the equivalent queries using objection outside of graphql they return as I’d expect.
Don’t have access to my code/SQL right now but can attach it later if it would be useful.
Anyways, Hope you can answer that for me.
Thanks again for building and releasing this project. Making my transition from rest pretty painless.
Kind regards,
Matt
Hi Ilya,
It would be really useful to have a simple mechanism to support caching. For example for SQL data that rarely changes but needs to be loaded often within the graph.
Thinking the r.model could be changed to support this with additional methods something like:
// return string | undefined
cacheKey = (context) => undefined;
// return existing data | query
preFetch = (context, query, cacheKey) => query;
// callback to potentially store in cache
postFetch(context, data, cacheKey) {}
With changes to the r.graph to support the above.
Keen to hear any thoughts.
Kind regards,
Matt
Hi there,
Still loving this library. Super useful and working well.
In the filters area you have this comment:
"TODO: lt, gt, lte, gte, like, ilike, contains, icontains"
Is that something you're likely to consider adding? Those would be useful to have available.
Thanks in advance,
Matt
So for the little story - I was converting objection-graphql to typescript.
Started my journey, and found this pkg by mistake.
Thank you! this is actually better than I could have ever done.
See #13, a query against a missing field results in runtime exception:
TypeError: Cannot read properties of undefined (reading 'fieldsByTypeName')
at resolve_tree (/usr/app/node_modules/objection-graphql-resolver/src/resolver/graph.ts:62:30)
which is rather cryptic. The message should be improved and suggest user what to do.
Hi there,
I've just recently upgraded from 5 -> 7 using the objection variant.
Everything seems to be going really well apart from one small change that I had to make:
I have a few examples of the following in my codebase:
// Select all objects in -to-many relation
comments: true,
// Select all objects in -to-many relation
all_comments: r.relation({
modelField: "comments",
.... etc
When I attempted to query this relationship I get the following error:
query ($id: ID!) {
post(id: $id {
all_comments {
...
}
}
}
[15:56:39.676] ERROR (52345): unknown relation "all_comments" in an eager expression
err: {
"type": "GraphQLError",
"message": "unknown relation \"all_comments\" in an eager expression",
"stack": ...
To get around this I had to explicitly add "all_comments" as a new relationship to my model:
class Post extends Model {
static get relationMappings() {
return {
comments: ...,
all_comments: ...,
}
}
After making the above change it worked again as expected. I've not had to do that previously so I'm assuming it is a minor bug.
My versions are:
├── [email protected]
├── [email protected]
Thanks for any help with this.
Kind regards,
Matt
How would I go about filtering results where the condition of a nested relationship exists? I've looked through the docs and tried various approaches to no avail.
Example:
Let's say we have Authors and Books, and we want to filter Authors by existence of a Book with a genre.
{ id: 1, name: "John" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Mary" }
{ id: 1, author_id: 1, title: "Foo", genre: "Fiction" },
{ id: 2, author_id: 1, title: "Bar", genre: "SciFi" },
{ id: 3, author_id: 2, title: "Some Book", genre: "Non-Fiction" },
{ id: 4, author_id: 3, title: "Other Book", genre: "Fiction" },
{ id: 5, author_id: 3, title: "Another Book", genre: "Non-Fiction" },
{ id: 6, author_id: 3, title: "Yet Another Book", genre: "SciFi" },
Suppose we query Authors where genre = Fiction, only Author ID 1 and 3 are returned.
In my real use case, it's not Authors and Books, but Assets and Events.
Query:
query Assets($limit: Int, $offset: Int, $eventsFilter: Filter) {
assets(limit: $limit, offset: $offset, filter: $filter) {
name
events(filter: $eventsFilter) {
id
event_type
}
}
}
Variables:
{
"limit": 5,
"offset": 0,
"eventsFilter": {
"event_type": "created"
}
}
returns:
"data": {
"assets": [
{
"name": "Squish #4644",
"events": []
},
{
"name": "Squish #4651",
"events": []
},
{
"name": "Squish #4667",
"events": []
},
{
"name": "Squish #4676",
"events": []
},
{
"name": "Squish #4684",
"events": []
}
]
}
}
But I want these assets to be excluded, because they don't meet the Events criteria.
First off -- awesome project! I literally started walking down the steps of building this myself as I wanted to support naive resolution without needing to reference the entire tree at each top level query. And then a bit of googling later, found myself testing this out =)
Anyway, more of a question than anything. I'm curious if the relationMappings
getter is required on all models? I was really struggling to get this package working until I dove in and looked around get_field_resolver
and saw that my models weren't looking the same and lookups were failing.
It could be cool to expand the readme with some examples that include the objection models. I'm working on a scaffold right now that could be a source of content for something like that if you're interested.
Cheers!
Currently, selecting a custom field with raw sql looks like this:
r.table(
db.post,
{
fields: {
preview: (q) =>
q.select({ preview: q.sql<string>`substr(text,1,100)` }),
This is too wordy, and, it breaks when a field is requested with an alias:
posts {
my_preview: preview # will not work
}
The syntax should rather be:
r.table(
db.post,
{
fields: {
preview: (q) => q.sql<string>`substr(text,1,100)`
with orchid-graphql wrapping the field select.
Hi again IIya,
I'm totally loving this library. Making my GraphQL project super productive. Have found another issue however.
All of my model resolvers have this modification added to exclude any virtually deleted records
const defaultOptions = {
modifier: (query, args) => query.whereNull('deletedAt')
}
I.e.
export const Person = ModelResolver(PersonModel, defaultOptions)
export const Company = ModelResolver(CompanyModel, defaultOptions)
export const Office = ModelResolver(OfficeModel, defaultOptions)
Assuming a person works at a company and a company has multiple offices.
When I query the above I get a SQL error saying:
and deletedAt
is null - Column 'deletedAt' in where clause is ambiguous",
Hope that's something you could take a look at.
Cheers,
Matt
GraphResolver should allow to setup defaults for its model resolvers:
const resolveGraph = GraphResolver({
models: {
Foo: ModelResolver(FooModel) // can access all fields in Foo
},
defaults: {
allowAllFields: true, // <--- THIS: naming should be consistent with #11
}
})
const resolveGraph = GraphResolver({
models: {
Foo: ModelResolver(FooModel, {
fields: {
bars: true,
},
}),
Bar: ModelResolver(BarModel, {
allowAllFields: true, // see #11
}),
},
defaults: {
filter: true, // <--- THIS: should be consistent with #9
}
})
then filters will work on all relations to bars:
query {
foo(id: 1) {
bars(filter: { status: "pending" }) {
id
}
}
}
Note that this will introduce two breaking changes:
options
argument).allowAllFields: false
(the proposed default) on GraphResolver, non-configured model resolvers will deny access to all their fields (unlike now, when they enable it by default). This could in theory be solved with something like allowAllFields: "unless-configured"
but I don't see much value in that (current) behaviour other than migrating existing projects.Currently, ModelResolver returns a pre-configured closure which is non-customizeable. It should therefore either become a higher-level closure which is resolved during GraphResolver instantiation, or accept these defaults as a runtime option.
See #4 (comment):
it'd be helpful to export the helper that filters query against a filter object such as { field__in: [1,2] }
.
Sample code:
query FilteredAssets($limit: Int, $offset: Int, $events_filter: Filter) {
assets(limit: $limit, offset: $offset, filter: $filter, eventsFilter: $eventsFilter) {
name
# events could be included or could be omitted - that doesn't affect the assets query!
events(filter: $eventsFilter) {
id
event_type
}
}
}
Resolver:
Assets: async (parent, args, context, info) => {
const assets = await resolveGraph(
context,
info,
Asset.query().whereExists(
Assets.relatedQuery('events').where(
// construct custom condition from args.eventsFilter
// <------ TODO: use the newly exported function
)
)
)
return assets
}
Hi Ilya,
Thanks for building this great looking library. Looking forward to using it with my project.
I noticed your documentation, examples and tests make no mention of mutations.
Is that because it’s not currently supported? Or if it is could you add an example scenario to the documentation?
Thanks in advance,
Matt
I'm trying a fairly straightforward example with pagination, but am running into the following error: Cannot read properties of undefined (reading 'fieldsByTypeName')
It's failing on line 62 of graph.ts
because in line 61 tree
is being assigned to tree.fieldsByTypeName[type][field]
, but field has been set to "nodes" in cursor.ts
line 112, and there are no fields named "nodes" on the entity so tree is assigned undefined.
I've tried instantiating CursorPaginator
with no options, and with different take
and fields
options
If there is other useful information I can add here, please let me know.
const resolveGraph = GraphResolver({
Plan: ModelResolver(Plan)
})
const plans = await resolveGraph(context, info, Plan.query(), {
paginate: CursorPaginator()
})
'use strict'
const { Model } = require('objection')
class Plan extends Model {
static get tableName () {
return 'plans'
}
static get relationMappings () {
return {
services: {
relation: Model.ManyToManyRelation,
modelClass: require('./Service'),
join: {
from: 'plans.id',
through: {
from: 'plansServices.planId',
to: 'plansServices.serviceId'
},
to: 'services.id'
}
}
}
}
}
module.exports = Plan
Resolver:
export const Org = r.table(db.org, {
fields: {
id: true,
name: true,
operator_id: 'operatorId', // graphql field differs from db table field
},
})
Query:
query orgs($operator_id: ID) {
orgs(filter: { operator_id: $operator_id }) {
id
name
}
}
Expected: filter works.
Actual result: column "operator_id" does not exist
With #15 adding more filters, we can add a new dimensions for filters: negation.
Example:
id__not: 5
data__not: null
text__not__like: "%foo%"
id__not__in: [1, 2, 3]
Howdy again,
Following the docs for filtering I'm attempting to filter at a deep level only.
When I attempt to call queries that involve several levels of joins such as the following the query isn't amended to include the filter I've included and returns all of the records.
I've switched on filter: true for all models and set it at the resolver level, and updated the GQL to include the filter options.
I notice your docs state "TODO" against some of the extra filtering options. Should this case work currently? I've tried other variants like status__in: [pending] and adding a modifier to the model directly and calling that.
{
"query": "query NotWorkingExample {
companies {
offices {
staffMembers {
firstName
lastName
expenses(filter: {status: pending}) {
id
status
}
}
}
}
}"
}
EDIT: added code formatting - Ilya
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.