graphql
graphql copied to clipboard
Accessing Field Metadata
I'm wondering if it's possible or the best way to be able to access field metadata for a given type.
I know internally PartialType uses getFieldsAndDecoratorForType which loads and compiles metadata (performance issue if I use the type helpers a lot???.. different issue).
However, I want to get a field's GraphQL type to create other types.
Quick Sudo-Ish Code:
function createFilter<T, K extends keyof T>(Type: Constructor<T>, props: K[]): Constructor<FilterInterface<T, K>> {
@InputType(`${Type.name}AbstractFilterOperators`, { isAbstract: true })
abstract class Filter {}
const getPropFilter = (prop: string) => {
// looks up Type's decorated `@Fields()` to figure out which custom input type to use
}
props.forEach(prop => {
const PropFilter = getPropFilter(prop);
Object.defineProperty(
Filter.prototype,
prop,
{
value: new PropFilter(),
writable : true,
enumerable : true,
configurable : true
}
);
Field(() => PropFilter)(Filter.prototype, prop as string);
});
return Filter as Constructor<FilterInterface<T, K>>;
}
How can I get all the decorated @Field()s for a given class before everything is compiled?
getFieldsAndDecoratorForType which loads and compiles metadata (performance issue if I use the type helpers a lot???.. different issue).
We've been tracking this in a separate issue (we had a significant impact on the performance in the past), but it's fixed now, and using getFieldsAndDecoratorForType (+passing a type reference as an argument) should be safe.
How can I get all the decorated @Field()s for a given class before everything is compiled?
This isn't doable (compileClassMetadata() call is required).
I ended up just using the PartialType helper since it’s public to generate the needed metadata and it works, however, would be nice to have the internal stuff be public if at all possible. Or even if there was a less structured metadata type added at the decorator level to later be turned into metadata. In my library I store plain definitions, which is as close to the decorator options as possible, then later build off that.
Is there a potential issue in compiling a class early?
I essentially made a helper to take on a type and create a query filter based on passed in fields for queries. It works and is super convenient. Essentially, I too would like to create a version of “PartialType”.
On Fri, Feb 5, 2021 at 12:46 AM Kamil Mysliwiec [email protected] wrote:
getFieldsAndDecoratorForType which loads and compiles metadata (performance issue if I use the type helpers a lot???.. different issue).
We've been tracking this in a separate issue (we had a significant impact on the performance in the past), but it's fixed now, and using getFieldsAndDecoratorForType (+passing a type reference as an argument) should be safe.
How can I get all the decorated @field https://github.com/field()s for a given class before everything is compiled?
This isn't doable (compileClassMetadata() call is required).
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/nestjs/graphql/issues/1361#issuecomment-773887946, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEP4YWJBNKLTBRAKKLGBB3S5OWAHANCNFSM4XDUKXCQ .
Sorry, I responded from my phone.
We've been tracking this in a separate issue (we had a significant impact on the performance in the past), but it's fixed now, and using getFieldsAndDecoratorForType (+passing a type reference as an argument) should be safe.
Looking at the *Type helpers, since they compile the metadata for the class each time, I see performance issues at runtime generation if someone is using those helpers a lot (we do) with a lot of types (we do), so types are compiled multiple times?
I'd be curious on an actual benchmarks, but in our infrastructure where a server can boot up and shut down at any given moment and need to scale really quickly, I wonder if it's an issue.
The easiest solution would be to compile/do heavy lifting once per class and cache it and on consecutive calls, return the cached instance and don't allow any mutations to metadata after that without a clearMetadataFor*() type call. Probably a lot of work and can break stuff for a small fraction of users though.
Again, separate issue.
...
Back to field metadata...
Will there be a future way of safely accessing metadata in user-land without having to import @nestjs/graphql/lib/.... I suppose all that's needed ATM is exporting getFieldsAndDecoratorForType from the main package. Not sure if you'd want to bloat that though (even though other internals are exported).
Looking at the *Type helpers, since they compile the metadata for the class each time, I see performance issues at runtime generation if someone is using those helpers a lot (we do) with a lot of types (we do), so types are compiled multiple times? I'd be curious on an actual benchmarks, but in our infrastructure where a server can boot up and shut down at any given moment and need to scale really quickly, I wonder if it's an issue.
As I've mentioned earlier, this was actually causing performance issues in the past and there's a corresponding issue with a repository in which the author generates ~10000 types (? as far as I remember). Previously, the time required to compile types was growing exponentially, but now (after fixing the root cause) if you run it, it should be pretty smooth.
Will there be a future way of safely accessing metadata in user-land without having to import @nestjs/graphql/lib/.... I suppose all that's needed ATM is exporting getFieldsAndDecoratorForType from the main package. Not sure if you'd want to bloat that though (even though other internals are exported).
This sounds doable as a short term solution.
I can add a concrete use-case. Currently I generate input types based on object types for "filter" input. It's the same that PartialType does, but make arrays from the properties types.
As seen below, I abuse PartialType to create dummyPartialType and to be sure the metadata is available for the object type and it's properties.
Would love to see this somehow supported in a clean way to create custom mapped types.
import { Type } from "@nestjs/common";
import { Field, InputType, ObjectType, PartialType, TypeMetadataStorage } from "@nestjs/graphql";
import { PropertyMetadata } from "@nestjs/graphql/dist/schema-builder/metadata";
@ObjectType({isAbstract:true})
class BaseModel {
@Field()
id!: string
}
@ObjectType()
class TestModel extends BaseModel {
@Field()
test!: number
}
// make all string or number properties as optional array based on provided M type
type ModelFilter<M > = { [P in { [Key in keyof M]: M[Key] extends (string|number) | undefined ? Key : never; }[keyof M]]?: M[P][] | undefined; }
function inheritClassFields(
target: Type<unknown>,
properties: PropertyMetadata[]
): PropertyMetadata[] {
try {
const parent = Object.getPrototypeOf(target);
if (parent === Function) {
return properties;
}
const objectMetadata = TypeMetadataStorage.getObjectTypeMetadataByTarget(
parent as Type<unknown>
);
const parentProperties = objectMetadata?.properties || [];
return inheritClassFields(parent, [...parentProperties, ...properties]);
} catch (err) {
return properties;
}
}
export function PartialArrayType<M >(
modelClass: Type<M>
) {
@InputType({ isAbstract: true })
abstract class InternalAbstractFilterModelClass {}
// workaround to force nestjs providing related metadata in TypeMetadataStorage
const dummyPartialType = PartialType(modelClass);
let { properties } =
TypeMetadataStorage.getObjectTypeMetadataByTarget(modelClass) || {};
if (properties === undefined) {
throw new Error(`no type metadata exists for ${modelClass.name}`);
}
properties = inheritClassFields(modelClass, properties);
properties.forEach((propertyMetadata) => {
Field(() => [propertyMetadata.typeFn()], { nullable: true })(
InternalAbstractFilterModelClass.prototype,
propertyMetadata.name
);
});
return <Type<ModelFilter<M>>>InternalAbstractFilterModelClass;
}
@InputType()
class TestFilterModel extends PartialArrayType(TestModel) {}
The TestFilterModel class looks then as defined (including the InputType and Field decoractors):
class TestFilterModel {
id?: string[]
test?: number[]
}
My use case for this functionality being more widely available is for an abstract Base Resolver. In my Base Resolver I’m decorating functions with the ResolveField decorator for every relation in a given model. This makes it easier to resolve any model in my database from GraphQL that has a Field decorator.
The fields returned by getFieldsAndDecoratorForType allow me access to proper typing, name, and options.
Any update on exporting this helper? Would a PR to expose this be okay, or do you have some other plan?
It would be great if we can expose getFieldsAndDecoratorForType.
Another use case - I want to be able to extract the requested fields of a resolver (using graphql-fields-list), but differentiate between the model fields and the resolved fields, so I can build an accurate DB query with only the DB fields.
I would be happy to make a PR if that is something the maintainers ( @kamilmysliwiec ) still belive should be done