-
-
Notifications
You must be signed in to change notification settings - Fork 676
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
MetadataStorage: Access to unbuilt metadata #134
Comments
First of all, accessing internal stuff is strongly prohibited. They are a subject of constant changes and they might be renamed, moved or even removed without any notice. To prevent that kind of problems, I will switch to publishing the package bundled with rollup, so there will be no way to access internal, private stuff. Users can safely interact only with things that are exported in index file. And yes, I've described the drawbacks of current metadata storage architecture and how I'm going to improve it here: #133 (comment) For your export function UserClass(nullable: boolean) {
@InputType(nullable ? "UpdateUser" : "CreateUser")
class User {
@Field({ nullable })
name: string;
@Field(type => Int, { nullable })
age: number;
}
return User;
}
export const CreateUser = NullableHOC(UserClass, false);
export const UpdateUser = NullableHOC(UserClass, true);
export type CreateUser = InstanceType<typeof CreateUser>;
export type UpdateUser = InstanceType<typeof UpdateUser>; This allows you to run this operations: mutation {
addUser(data: { name: "name", age: 2 })
updateUser(name: "name", data: { age: 3 })
} I should probably better document this feature 🤔 export function NullableHOC<T>(
hoc: (nullable: boolean) => T,
nullable: true,
): HOCClass<typeof hoc, typeof nullable>;
export function NullableHOC<T>(
hoc: (nullable: boolean) => T,
nullable: false,
): HOCClass<typeof hoc, typeof nullable>;
export function NullableHOC<T>(hoc: (nullable: boolean) => T, nullable: boolean) {
return hoc(nullable);
}
export type InstanceType<T> = T extends ClassType<infer R> ? R : never;
export type HOCClass<T extends Function, U extends boolean> = T extends (...args: any[]) => infer R
? U extends true ? ClassType<Partial<InstanceType<R>>> : R
: never; |
The whole point of my suggestion was to make the metadata API public, not to advocate accessing internals. The mentioned problem / snippet was just to illustrate the point of what I needed to access and why. Your solution to the mentioned problem is elegant and I am thankful for it but it is not something I am trying to discuss here. I know that you are planning on redesigning the way how the metadata gets handled. Thats why I wanted to suggest other possible feature requirements for you to consider. The mentioned problem is solvable using HoC but it is not a silver bullet. Anything that needs more than parametrizing few values can be messy. For example lets say I would like to create filter input type for it with different operators for each field based on it's type. I believe that exposing input / object type metadata can be beneficial and I don't see much downside to it. If the API is designed well there is no need to hide it. |
I agree with that. But it's not the right time for it as it's a subject of constant changes - do you want breaking changes and major releases every week that would force you to change your code relying on metadata? I guess not 😉
From my experience, doing such complicated things like you presented with create/update type or filter type with parametrizing few values, just in favor of reducing boilerplate, is only a source of bugs. There's a lot of magic in this dirty tricks, which makes things incomprehensible for other teammates and that becomes a real pain while trying to debug it later when some problems appear. And I would prefer exposing dedicated tools with nice API for solving problems like: You are not the first who wants to generate the partial type or needs generic type, so it's better to report it, discuss the possibilities and create a solution that others could use too, rather than solving it by your own. |
I am upholding the decision to not expose the internal stuff. For now all the use cases of exposing metadata storage is about creating derivative types. So I think it's better to create a dedicated API for that case, rather than digging in internals or use weird hooks and workarounds to achieve the same.
This is the core problem - that's why metadata storage has phase. At first we collect just the data from decorators, then we can build the complete metadata structure of every target. E.g. during evaluation of generic abstract resolver class we know nothing about the class that is extending it. So getting full data need some hook that would be fired when all metadata from decorators is captured and before |
I'd also like to tap into the metadata. The thing I'm trying to solve is to dynamically generate the following from my schema:
...so that my API has extremely strong conventions. Your workaround above doesn't solve this issue. It would make more sense to tap into the schema generation step to read from the metadata and dynamically add to the graphql schema. |
The problem with
is that when doing the manipulation programatically, you loose all the typings, so you end up with redeclaring the interfaces for The better solution for derived types are types operator - Also the base types idea might be also helpful in managing this common relation between object type and input (subset of fields): class BaseUserData {
@Field()
name: string;
@Field(type => Int)
age: number;
}
@ObjectType()
class User extends BaseUserData {
@Field(type => ID)
id: string;
@Field(type => [Post])
posts: Post[];
}
@InputType()
class UserCreateInput extends BaseUserData {
@Field()
someInputField: string;
}
@InputType()
class UserUpdateInput extends Partial(UserCreateInput) {}
@Input()
class UserWhereInput extends Partial(UserCreateInput) {
@Field(type => [String])
ids: string[]
} This example is not really a good real example but just to show the API proposal 😉 |
@19majkel94 I appreciate the thoughtful write-up and good point about the types - I want those to be available, too. Currently, I'm achieving everything through type-graphql decorators like so:
You can see that there is a lot of redundancy here and plenty of opportunity for developers to accidentally introduce inconsistencies in the API. This is the reason I'd like to auto-generate this piece. If you know of a magic way to do so within the framework that would be great. Otherwise I might need to just read the model metadata and auto-generate the schema and typings for this piece. What I'm essentially trying to build is a Prisma-like framework that gives us a lot more control over our data models, but auto-generates all of the inputs and enums for consistency and velocity. |
The main problem is that TypeScript types system doesn't allow for creating dynamic property names:
So as long as we can do this: const prefix = "User";
interface UserFindOptions {
[prefix]: boolean;
} we can't do this: const prefix = "User";
const createUser = `create${prefix}`;
interface UserFindOptions {
[createUser]: string;
} So we're not able to create a generic or a type operator that would generate this kind of interface. But in your example, you can just use nested inputs! 😉 @InputType()
class UserWhereInput {
@Field({ nullable: true })
firstName?: StringSearchOptions;
@Field({ nullable: true })
lastName?: StringSearchOptions;
@Field({ nullable: true })
email?: StringSearchOptions;
}
@InputType()
class StringSearchOptions {
@Field({ nullable: true })
eq?: string;
@Field({ nullable: true })
contains?: string;
@Field({ nullable: true })
startsWith?: string;
@Field({ nullable: true })
endsWith?: string;
} So to make that typesafe, we can create a base type to implement: type WhereOptions<T extends object> = {
[P in keyof T]: StringSearchOptions | NumberSearchOptions | null;
};
@InputType()
class UserWhereInput implements WhereOptions<User> {
@Field({ nullable: true })
firstName: StringSearchOptions | null;
@Field({ nullable: true })
lastName: StringSearchOptions | null;
} And this one would be easier to create dynamically using type transform operators or derived type util, as the property keys are known at the compile time. For now you would have to setup a template and generate this code files from the model definition to avoid additional manual work. |
@19majkel94 - thanks again for all of your detailed and thoughtful write-ups. It gives me a lot of confidence using your library. Your idea gets things one step closer to being fail proof, but still presents the opportunity for developer error. So I actually went ahead and did the auto-generation approach as you said and it works like a charm. It requires the schema generation step to be separate from the app running, but I'm happy to work around that to enforce consistency in my APIs. Thanks again for all of the thought you've put into my problem! |
FYI, for anybody following the 2nd half of this thread looking for something like I've outlined above, I implemented it in a new library Warthog that composes TypeGraphQL and TypeOrm, then layers these conventions on top. |
This comment has been minimized.
This comment has been minimized.
@MichalLytek can you please give an example how we can use this where filter in find() method or typeorm?
|
@MichalLytek I'm dealing with having a ton of duplication between my ObjectTypes and Create and Update InputTypes. I think your NullableHOC strategy would help but I'm having a tough time wrapping me head around it. You mentioned documenting that feature. Any chance I could get more detail on how that works? |
The current
MetadataStorage
works in two phases.The problem is that if I want to access the metadata (ie. defined fields) anytime before the schema generation occurs I cannot do that because it was not built yet. This prevents use-cases like generating dynamic input types / object types based on existing metadata.
Example of the problem:
I have
CreateUser
input type and I want to createUpdateUser
input type which would have exactly the same fields asCreateUser
but all nullable (basically GraphQL equivalent to TSPartial<T>
).For this I would like to access metadata definition of
CreateUser
but I cannot do it directly because the metadata has not been built yet. Instead I have to access the information about input type's fields through access to private property.Here is a described helper with an workaround for current situation:
Ideal solution
I think we should rethink the way we store the metadata (at least for input / object types) and register them directly within a built structure of given target. We would have to think carefully about which information is available at given time.
The minimum effort solution
If you think that the redesign of
MetadataStorage
is not worth the trouble (or will not be within next few versions), it would be nice to add at least this method to the public API ofMetadataStorage
.The text was updated successfully, but these errors were encountered: