Git Product home page Git Product logo

graphql-orm's Introduction

graphql-orm's People

Contributors

github-actions[bot] avatar ilyasemenov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

graphql-orm's Issues

Use graph.resolve for root query sub-fields

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.

Option to allow all filters for a model

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

Model resolver option to allow all fields by default

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

Clean model instances as a whole

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.

Dynamic configuration of table resolvers based on context

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.

pulling my head - Maximum call stack size exceeded

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)

Model modifier for root query and -to-many relations only

Currently, a model/table modifier runs for all queries against that model:

  • root query
  • -to-many relations
  • -to-one relations

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.

ambiguous column name in ManyToMany query

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

Feature Request: Basic caching support

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

filtering with >, <, >=, <=, etc.

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

Thank you!

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.

Improve error reporting for missing fields

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.

r.relations and adding extra relationships

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

Filter results by existence of relation

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.

Authors

{ id: 1, name: "John" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Mary" }

Books

{ 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.

Is `relationMappings` required on all models involved in relation-based queries?

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!

Simplify syntax for selecting fields with raw query

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.

ModelResolver modifier issue

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

Global defaults in graph resolver

GraphResolver should allow to setup defaults for its model resolvers:

  1. Allow or deny access to all model fields by default (see #11 as well):
const resolveGraph = GraphResolver({
  models: {
    Foo: ModelResolver(FooModel) // can access all fields in Foo
  },
  defaults: {
    allowAllFields: true, // <--- THIS: naming should be consistent with #11
  }
})
  1. Enable or disable filters on models by default (see #9 (comment) as well):
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
    }
  }
}

Breaking change

Note that this will introduce two breaking changes:

  1. GraphResolver args will become different (single options argument).
  2. With 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.

Implementation

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.

Export helper for custom-run filters

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
}

Mutations

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

Pagination error - Cannot read properties of undefined (reading 'fieldsByTypeName')

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

Filters to take into account simple field aliases

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

negating filters

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]

Enable filters in model resolver

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.