-
-
Notifications
You must be signed in to change notification settings - Fork 677
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
Circular dependencies bug #236
Comments
I don't really get what's the problem here. The modules are evaluated once, only the type function is called a two times (first time to check if it returns array, second time to actually get the time). What lazy loading you want to achieve with building server graphql schema? 😕 |
Sorry, some of my terminology got fuzzy here. Let me try to use some code as an example. Take a typical // models/post.ts
import Comment from './comment.ts';
class Post {
@Field(_type => [Comment])
comments: Comment[]; Now, when Node loads this file, it hits the The typical way this is solved is by using // models/post.ts
import Comment from './comment.ts'; // this is okay because Typescript will strip this out of the compiled JS since it's not used anywhere except type annotations
class Post {
@Field(_type => [require('./comment.ts')])
comments: Comment[]; However - the problem here is that TypeGraphQL actually calls the The change I've proposed in #237 appears to keep existing functionality the same, but simply delays invoking the |
I know what TypeGraphQL is doing. I wonder what your code is doing beside defining TypeGraphQL classes. What is you use case of module initialization that you suffer from this behavior and dynamic require fix that? The moment when I call the type getter makes no difference in term of "get an empty object rather than the Post class, since the models/post.ts file hasn't finished executing yet". This is how node resolvers circular module dependencies - one of them must be an empty object first, then it's replaced with actual exports after the depended module initialize. I just need to wait for the initialization before using (reading) the values, so that's why they are a type getter function in TypeGraphQL and in TypeORM that are called after the modules initialization.
This sentence is not true because TypeScript use this imported "types" (actually it's a runtime class) to reflect the design types ( import { User } from "./user";
class Recipe {
@Field(type => User)
@ManyToOne(type => User)
author: User;
} const user_1 = require("./user");
tslib_1.__decorate([
src_1.Field(type => user_1.User),
typeorm_1.ManyToOne(type => user_1.User),
tslib_1.__metadata("design:type", user_1.User)
], Recipe.prototype, "author", void 0); |
Unfortunately, this is a Node problem, but to deal with it is simple: Use a star import on an index file and access all classes under that namespace. This is to cheat the dependency graph as the namespace served as a global value to all possible class object references. This is also how you workaround cyclic TypeORM entity reference as well. Later when the type loader needs it, it will work because all modules are loaded. The downside of doing it is that this is very verbose and default import is disallowed. Other ways of dealing with this could be finding the feedback vertex set but this is a NP-Complete problem. |
Yep! To clarify - for TypeGraphQL's purposes, the current approach works. The problem I'm encountering is in my app code; TypeGraphQL isn't breaking. But the problem is that, while TypeGraphQL's approach works for TypeGraphQL, it much more difficult to avoid this category of problem in my app code.
Ah, great point! That's what I get for trying the code in the TS Playground first vs. checking my actual output 😉
I've tried to avoid describing my specific use case since there's lots of complexity that doesn't directly relate to the problem at hand. I'll try to describe it here in the simplest form I can that still reproduces the problem. The core problem is that I'm trying to model a polymorphic relationship by "denormalizing" it into a series of relationships with possibly null values. This structure creates circular imports. I have a few entities I'm working with:
A The resulting code looks something like: // entities/file.ts
import ProcessingTask from './processing-task';
class File {
@Field(_type => [ProcessingTask])
processingTasks; // entities/processing-task.ts
import File from './file';
import Video from './video';
class ProcessingTask {
@Field(_type => File)
file;
@Field(_type => Video)
video; // entities/video.ts
import File from './file';
class Video extends File { If
When Hopefully that helps make the situation a little clearer. Happy to jump on Gitter to hash this out in realtime if that's easier too! As a side note: what are there downsides you see to adopting #237? I certainly want to make sure we understand the problem, but there hasn't been any discussion yet of the fix, so I'm curious if there's something I'm missing that makes #237 a more difficult change than I thought. |
And just to clarify the problem and potential solution from my example: If we can delay that first step of the import loop, i.e. when If Normally the way to accomplish this is by switching from But TypeGraphQL makes this impossible, because the My PR (#237) changes the |
It's a dirty hack that rely on mutating the variable in unknown, later time.
I know that circular dependencies in node.js require are problematic but the well-adopted solution by many libs (i.a. mongoose, graphql-js) is to use thunks - functions that returns something, if it rely on other similar deps. So in
I know but I still don't know how the removing of return type function call helps your code because it's still required and evaluated in the same time because of the reflection... also, why you need to lazy call |
I'd suggest that the current code is equally hack-ish, since it rely's on Node's "mutate the object reference once it's ready" behavior. In some sense, my PR would improve the situation by waiting to retrieve the type value until we know something will be there.
I ended up typing the
Yep, but the key component here is that those other libraries all delay invoking the thunk. If the thunk is immediately invoked, then it loses its value, because the import/require is back on the synchronous execution path. Using your example of
To clarify - I'm not suggesting removing the function call entirely. I'm suggesting that we delay the call until it's absolutely necessary.
See above - if you type it as
My goal with the example above was to explain why the lazy require call is necessary. Is there a part of that example I can expand on that will help make that a bit clearer?
Sorry, a bit confused here since the tests are all passing on #237. Are you suggesting that lots of people may be relying on the internal behaviors of the framework (i.e. somehow their code relies on the return type function being called synchronously) and would be caught off guard by this change? If you're hesitant to make this change, mind if I ask how you'd suggest I get my example scenario working? Or does TypeGraphQL not have plans to support this kind of data model? I don't mind if it's a bit hacky (obviously, since my solution involves typing as |
So do I. The "fixed" part of the code is responsible only for checking for
What kind of data model? Circular dependencies between object types? It works, it's tested. What else do you need? Show me what you want to achieve, not how to fix the codebase. Then I can show you how to use or make a fix. |
Perhaps I'm misunderstanding, but it seems like you are not delaying the thunk invocation:
Am I misusing the
Apologies if my previous comments were unclear. I'll try to restate my example to clarify. I have the following classes:
That's what I'm trying to achieve - I'd love your help figuring out how to accomplish that with TypeGraphQL. As far as I can tell, it's not currently possible, but I'd love to be wrong! 😉 |
Do you have a repository with reproducible code example? Do you have an error log or description what's happening (other that "not working")? It will be really helpful with detecting the pattern you use and how the thunk delay is helping with it. |
I've tried to reproduce your case with File, Video and ProcessingTask. Three things:
I've also tried to disable emitting the metadata but without any luck. |
Ok, changing the thunk imports to Now I know what the use case is and why we need to fix that 😉 Closing via #237 🔒 |
Describe the bug
The current implementation of the
Field
decorator leads to attempts to synchronously load circular dependencies, resulting in imported modules beingundefined
.To Reproduce
https://github.com/davewasmer/typegraphql-circular-deps-test
Clone down, npm/yarn install, then run either:
yarn ts-node src/foo.ts
, oryarn ts-node src/bar.ts
The console output will show that the type function passed into
@Field
is invoked synchronously, resulting inundefined
for the imported file.Expected behavior
The
@Field
decorator would invoke the type function lazily (i.e. not in the synchronous load path of the module), allowing for properly defined circular dependencies.Enviornment (please complete the following information):
Additional context
There's a few relevant pieces of prior art here:
Both of these are concerned primarily with: "Can Type-GraphQL build an accurate schema with circular deps?" The answer is yes, as the tests demonstrate. It does this by repeatedly calling the type function, which lets it get the circularly referenced module once the cycle finishes loading.
However, the problem I'm describing here is not with Type-GraphQL's ability to build the schema, but rather the implications on the rest of my code. Because the
@Field
synchronously calls the type function, it makes it difficult to work with for the rest of my code, since I now have no way to lazily load the circular dependencies to avoid seeingundefined
.I believe patching
@Field
to ensure that the type function isn't called on the synchronous path should fix this issue. I'm investigating that now, but I'm pretty new to the codebase ...The text was updated successfully, but these errors were encountered: