-
Notifications
You must be signed in to change notification settings - Fork 65
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
Polymorphic queries -> discriminated union types #1090
Conversation
…ymorphic queries as discriminated union
I restored some of the old functionality, and gated the stricter types behind an opt-in via declaration merging. Just looking for ways to get this out without a major. But open to reverting it or other suggestions. |
3734b62
to
5b5d19c
Compare
This comment was marked as resolved.
This comment was marked as resolved.
5b5d19c
to
db0cbce
Compare
This comment was marked as resolved.
This comment was marked as resolved.
db0cbce
to
319a2cf
Compare
319a2cf
to
1ace1e0
Compare
… `__type__.name` is the proper path.
92f8e59
to
50c8560
Compare
50c8560
to
3be27a5
Compare
packages/generate/src/syntax/path.ts
Outdated
? ScalarType< | ||
"std::str", | ||
TypesystemOptions["future"]["strictTypeNames"] extends true | ||
? Root[typeof typenameSymbol] | ||
: string, | ||
Root[typeof typenameSymbol] | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the difference between Scalar's TsType
& TsArgType
. I was thinking that they were the output / input types respectively.
I don't understand why the input type would be called out here explicitly and set to the literal string - where/how would that be used be as an input?
I figured the output type remained as a string
for BC reasons. So I created another future option to make it strict.
This is for this btw:
scope => ({ t: scope.__type__.name })
But then I was thinking about this case
scope => ({ __type__: { name: true } })
Which had already made the output type a strict string.
So I thought the other case was a miss, but there is some explicit code here.
I guess all that to say I could just do this:
? ScalarType< | |
"std::str", | |
TypesystemOptions["future"]["strictTypeNames"] extends true | |
? Root[typeof typenameSymbol] | |
: string, | |
Root[typeof typenameSymbol] | |
> | |
? ScalarType<"std::str", Root[typeof typenameSymbol]> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scotttrinh thoughts here? I don't think we need a flag for this & just use my suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I'm not sure, but I'm fine with going with the suggested change for now and looking at what the fallout is.
ddd9c6f
to
530e5c6
Compare
530e5c6
to
b9eff07
Compare
…a type intersection expr
b9eff07
to
814962a
Compare
if (hasPolyEl && !seen.has("__typename")) { | ||
addLine("__typename := .__type__.name"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new functionality implies a __typename
field when needed. If the user specifies this and it doesn't match what is assumed then the types will be wrong. We could check the __typename
value here if it is already present (from a user) and confirm it is .__type__.name
or throw.
I couldn't figure out a clean way to do this because the actual generated EdgeQL explicitly references the scope, which I couldn't figure out how to access.
single __typename := __scope_1_defaultMovie.__type__.name,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, we can leave that as a follow up investigation, thanks for the details!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very solid 👏 Thanks for the huge effort here. Love that we now have some feature-flagging/opt-in machinery set up in the typesystem, too, that seems generally very useful.
if (hasPolyEl && !seen.has("__typename")) { | ||
addLine("__typename := .__type__.name"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, we can leave that as a follow up investigation, thanks for the details!
One thing I want to think about is maybe having some tests with the legacy behavior just to catch any regressions we make between now and when we switch the new behavior to the default in a future major release. I can add those. |
@@ -1299,6 +1301,7 @@ function shapeToEdgeQL( | |||
if (val.__kind__ === ExpressionKind.PolyShapeElement) { | |||
polyType = val.__polyType__; | |||
val = val.__shapeElement__; | |||
hasPolyEl = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, we don't actually have a runtime representation of the type-level "opt in" for this behavior, so this ends up still being an arguably breaking change since we will be returning more. Definitely arguable as to whether or not returning strictly more data is breaking, but anyone who currently has tests for the existing behavior will get failures.
I wonder if we can make a runtime configuration, and actually infer the type-level config from that runtime config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably recommend a generator config flag in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, agreed. we can even have it generate/copy a different typesystem.ts file to avoid the overhead of being conditional in the types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably wouldn't say that adding an additional field is a breaking change. User tests may not break. Only if they are asserting exactly or using snapshots.
But I can see how it's not black/white. IMO none of this stuff is. kinda like changes in TS releases lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User tests may not break. Only if they are asserting exactly or using snapshots.
I think this is pretty common: make a query (or use a data layer more likely) and assert that the returned data is precisely these objects. I agree in general that if you are introducing new properties/data in your own application, maybe you don't consider that a breaking change, but this might warrant a little more care to avoid tripping up people when upgrading point releases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you looking for a CLI flag for this as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think we should add it, like --enable-polymorphism-as-discriminated-unions
as silly as that sounds. This is non-trivial, but I think it's worth it to avoid going with a major version bump here. I'm honestly open to the major version bump, but that will absolutely delay getting this released.
If you think you have the bandwidth to add the CLI flag, let me know. Otherwise, I'll stick it in the queue and try to get to it (or maybe have @diksipav pick this up) ASAP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--future-polymorphism-as-discriminated-unions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, that was my first thought, but then I thought maybe in the next major we could allow you to still opt-into the old behavior, but now I'm thinking there is no advantage to keeping the old behavior around in the next major.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't see a reason to keep the old behavior long term.
1805d33
to
a2f2c07
Compare
/** | ||
* Use declaration merging to set the {@link TypesystemOptions} via this | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
export interface SetTypesystemOptions {} | ||
|
||
export type TypesystemOptions = { | ||
future: { | ||
/** | ||
* Opt-in to the new discriminated union functionality for polymorphism. | ||
*/ | ||
polymorphismAsDiscriminatedUnions: SetTypesystemOptions extends { | ||
future: { polymorphismAsDiscriminatedUnions: true }; | ||
} | ||
? true | ||
: false; | ||
/** | ||
* Opt-in to strict __type__.name string literal unions. | ||
*/ | ||
strictTypeNames: SetTypesystemOptions extends { | ||
future: { strictTypeNames: true }; | ||
} | ||
? true | ||
: false; | ||
}; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this or can we use import future from "./future";
and get the type from the import?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it depends on what the DX goal is.
If the goal is to just use the cli flag and expect that future.ts
is statically overridden, then no we don't need TypesystemOptions
or the declaration merging on SetTypesystemOptions
.
If the goal is to allow this to be set dynamically. Then yes we still need it.
In that case, devs would set the futures to true.
future.polymorphismAsDiscriminatedUnions = true;
future.strictTypeNames = true;
And then denote to TS that they intend to do that with the declaration merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it makes sense for this to be dynamic in any way at all. You're either opting into this future behavior and will refactor all of your code to match it, or you're not. I also care about making sure the runtime behavior and types are aligned, so allowing (or really I guess encouraging) both seems dangerous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then yeah we don't need any of this. Just base it all of the futures object like you suggested
computeObjectShape< | ||
Pointer["target"]["__pointers__"] & Pointer["properties"], | ||
Element, | ||
TypesystemOptions["future"]["strictTypeNames"] extends true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So like,
TypesystemOptions["future"]["strictTypeNames"] extends true | |
(typeof future["strictTypeNames"]) extends true |
etc
case "--future": | ||
options.future = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is sufficient. We need to be able to specify which futures you are opting into. I could be ok with future A and have updated my codebase to account for it. But then future B is added and I would be automatically opted into it as well which is a breaking change I wasn't ready for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, @diksipav and I talked a bit about this yesterday and here are my thoughts:
- From the perspective of a general feature of the generator package, this
future
behavior is similar to TypeScript's ownstrict
behavior, which if you opt into directly is breaking between versions. It stands for some set of more granular configuration values, so I think this is useful in general. - From the perspective of this future behavior, I expect most (all?) folks to want to opt into both, but also would be fine exposing granular flags for each here, too. I don't want to add tests for each permutation since we plan on making this the only behavior in a future breaking change, but that does mean there might be bugs or poorly understood side-effects of having one or the other enabled, but not both.
I don't think we need to block on this, though, and I'm fine with trying to land this as-is now. I'll leave it up to @diksipav to decide if she wants to address this in this PR specifically, or as a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey lets merge this (once it's ready) and I'll create right away a follow up PR with individual future flags
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scotttrinh I'll merge this and start working on the PR with individual flags, if u think we should "swap" tests will do it in the next PR
I revived #508
Fixes #454
Probably a breaking change since the types are getting stricter.