type-graphql
type-graphql copied to clipboard
Fields decorator to inject parsed AST info
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 (https://github.com/robrichard/graphql-fields/issues/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.
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?
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) {
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 😉
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 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:
- 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. - 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 totype-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.
- Use
graphql-list-fields
or fork ofgraphql-fields
integrated (means copypasted) intotype-graphql
codebase - Naming:
@InfoFields
,InfoFieldsOptions
,InfoFieldsProjection (as tree)
&InfoFieldsList (as List)
- Ability to return array of fields as same
@FieldsInfo
decorator overload with shortcircuit if we're going with own implementation - Mapping fields names - you mean renaming to eg. typeorm entity shape, yes?
- 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? - Options. Should we use this
path
lens fromgraphql-fields-list
- I would say NO, separation of concerns, but... - 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 :upside_down_face:
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.
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.
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
}