Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fields decorator to inject parsed AST info #10

Open
MichalLytek opened this issue Jan 27, 2018 · 8 comments
Open

Fields decorator to inject parsed AST info #10

MichalLytek opened this issue Jan 27, 2018 · 8 comments
Labels
Enhancement 🆕 New feature or request Good First Issue 👋 You can start contributing here Help Wanted 🆘 Extra attention is needed
Milestone

Comments

@MichalLytek
Copy link
Owner

MichalLytek commented Jan 27, 2018

Some people instead of batching and caching separate resolvers (#51) prefer to load all the data manually. So when they resolve the query, they parse the resolve info object to get a list/structure of fields to fetch from DB or API and then create specific query to DB/API.

Also it might be useful for selecting fields/columns from DB, e.g. when the GraphQL query requires only a single field but our database SQL query has to be universal so it has * so it returns a lot of unnecessary data, like other 30 columns/fields.

It would be nice to have a decorator for that with nice integration with graphql-fields. It should convert the returned object to a mongo-like convention (robrichard/graphql-fields#5) and have an ability to return array of keys in first level (without nesting) for sql queries optimization.

It should also take care about mapping the GraphQL field names (provided with { name: "fieldName" } option) to the TS property names.

@MichalLytek MichalLytek added the Enhancement 🆕 New feature or request label Jan 27, 2018
@MichalLytek MichalLytek added this to the 1.0.0 release milestone Jan 28, 2018
@oeed
Copy link

oeed commented Dec 10, 2018

Is there a (possibly hacky) way to achieve this behaviour currently? It is something I'd really like to be able to use but seems like it's not going to be implemented for a while yet. Or, if it's straight forward enough, would making a PR for it be suitable?

@oeed
Copy link

oeed commented Dec 11, 2018

For anyone else looking for this, it wasn't documented too much but I found the @Info decorator which exposes the data, which you can then parse as mentioned here.

For example:

async deliveries(@Arg("filters") filters: DeliveryFilters, @Ctx() context: AuthenticatedContext, @Info() info: GraphQLResolveInfo) {

@MichalLytek
Copy link
Owner Author

Yes, you can always fallback to the pure graphql-js and do the things manually. When #45 will be live, you can create your own decorator that will encapsulate the parsing and inject the fields object to the resolver method, like described in this issue 😉

@MichalLytek
Copy link
Owner Author

Thanks to #329, now it's possible to implement this feature 🎉

If someone want to try, please discuss the API and interfaces here before submitting the PR 😉

@MichalLytek MichalLytek added Good First Issue 👋 You can start contributing here Help Wanted 🆘 Extra attention is needed labels Aug 22, 2019
@vadistic
Copy link

vadistic commented Sep 30, 2019

@MichalLytek I'm kinda not sure there is a need for lib decorator when small custom function suffice. But I'm coming from FP, and trying to get used to DI so there's that :)

Parsing ResolveInfo into single sql query is something I'm trying right now so - I can help with this PR.

Regarding graphql-field there are two options:

  1. There's https://github.com/Mikhus/graphql-fields-list with some cool features (path param as lens on returned tree) - but it would be another dep.
  2. I've just make a graphql-fields fork and refactored to TS. https://github.com/vadistic/graphql-fields-ts/blob/master/src/index.ts I've cut argument parsing. If I also cut current @skip/include decorator logic it's under 100 LOC so I would propose adding this directly to type-graphql codebase. It pass all original tests and is a bit simplified.

It should convert the returned object to a mongo-like convention (robrichard/graphql-fields#5)

Both options do exactly that. With one in the end.

and have an ability to return array of keys in first level (without nesting) for sql queries optimization.

It's just Object.keys(). Deep traverse could be shortcircuted for tiny optimisation. You would like to have this as overload, yes? (ok, no decorator return annotations so as 2 provided return types?)

It should also take care about mapping the GraphQL field names (provided with { name: "fieldName" } option) to the TS property names.

Do you mean like transform/ rename option?

const transform = {
   graphqlFieldName: "entityFieldName"
}

myField(@InfoFields({ transform } fields: InfoFields) {
   ...
}

Should it be global or scoped with lodash paths? On both sides?

// scoped with lodash paths on both sides

const transform = {
   "graphqlFieldName.Type.OtherType": "entityFieldName.RenamedType.OtherType"
}

Proposed API

interface InfoFieldsOptions {
  // renames field (possibly as lodash paths) 
  transform?: { [field: string]: string }
  // returns array of top level fields overload
  asArray?: boolean
  // (optional, esspecially with `graphql-fields-list`)
  // scope and pick nested tree branch with lodash path
  path?: string
  // ignore (skip?) specified fieldnames
  ignore?: string[]
}

myField(@InfoFields({ options }) fields: InfoFieldsProjection ) {
   ...
}

Sorry for that many questions - I just need to know requirements, so I can get it to your style.

  1. Use graphql-list-fields or fork of graphql-fields integrated (means copypasted) into type-graphql codebase
  2. Naming: @InfoFields, InfoFieldsOptions, InfoFieldsProjection (as tree) & InfoFieldsList (as List)
  3. Ability to return array of fields as same @FieldsInfo decorator overload with shortcircuit if we're going with own implementation
  4. Mapping fields names - you mean renaming to eg. typeorm entity shape, yes?
  5. Should mapping/transform be global with fieldname? Scoped with lodash paths on the left side? On both? Or maybe nested object? Support all? ignore option array should probalby follow the same convention. Should it be array or tree projection (object, same as return type), both?
  6. Options. Should we use this path lens from graphql-fields-list - I would say NO, separation of concerns, but...
  7. Return types could be generic with partial mapped type on model. But it would be hard with global transform (I can map this but typescript would then burn laptops) and impossible with lodash paths. Also impossible with path lens.

Cheers 🙃

@MichalLytek
Copy link
Owner Author

Do you mean like transform/ rename option?

No, we have a rename field syntax:

@Field({ name: "bar" })
foo!: string;

It's designed for abstract generic types and resolvers but we should support that mapping in the @Fields decorator too.

I would propose adding this directly to type-graphql codebase

It's burdensome in maintenance, we should just use the existing tools and only apply some transformations on the results, not dealing with the AST, fragments and variables by our own.

@glen-84
Copy link
Contributor

glen-84 commented Jan 10, 2020

Here's a very simple decorator using graphql-parse-resolve-info instead of graphql-fields:

import {parseResolveInfo, ResolveTree} from "graphql-parse-resolve-info";
import {createParamDecorator} from "type-graphql";

function Fields(): ParameterDecorator {
    return createParamDecorator(
        ({info}): ResolveTree => {
            const parsedResolveInfoFragment = parseResolveInfo(info);

            if (!parsedResolveInfoFragment) {
                throw new Error("Failed to parse resolve info.");
            }

            return parsedResolveInfoFragment;
        }
    );
}

export {Fields};

Usage:

    @Query(() => [User])
    public async users(@Fields() fields: ResolveTree): Promise<User[]> {
        console.dir(fields);
    }

Mapping GraphQL field names to class property names is blocked by #123.

You'll probably also want to simplify the structure, depending on your database API.

@fromi
Copy link

fromi commented Nov 12, 2020

I would like to share the Field Decorator I just made: in combination with mongoose, it takes a field reference and returns the object I need with the minimum operations.

  • if I already have the object populated, it is returned as is.
  • otherwise, if I only need the _id, I return a basic object {_id: ref}
  • finally, if more fields are required, I fetch the object in the database

Maybe someone will find that useful.

import {DocumentType, Ref} from '@typegoose/typegoose'
import {GraphQLOutputType} from 'graphql'
import {parseResolveInfo, ResolveTree, simplifyParsedResolveInfoFragmentWithType} from 'graphql-parse-resolve-info'
import {Model} from 'mongoose'
import {createParamDecorator} from 'type-graphql'

export default function ResolveRef<T>(Model: Model<DocumentType<T>>) {
  return createParamDecorator(({root, info}) => {
    const resolveTree = parseResolveInfo(info) as ResolveTree
    const ref = root[resolveTree.name] as Ref<any>
    if (typeof ref === 'object' && ref._id) {
      return ref
    } else if (onlyRequiresId(resolveTree, info.returnType)) {
      return {_id: ref}
    } else {
      return Model.findById(ref)
    }
  })
}

function onlyRequiresId(resolveTree: ResolveTree, type: GraphQLOutputType) {
  const {fields} = simplifyParsedResolveInfoFragmentWithType(resolveTree, type)
  for (let field in fields) {
    if (field !== '_id') {
      return false
    }
  }
  return true
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement 🆕 New feature or request Good First Issue 👋 You can start contributing here Help Wanted 🆘 Extra attention is needed
Projects
None yet
Development

No branches or pull requests

6 participants
@glen-84 @oeed @fromi @vadistic @MichalLytek and others