-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Enables strict null checks in SDK #2360
base: main
Are you sure you want to change the base?
Conversation
@@ -302,7 +302,7 @@ function AdditionalFormFields({ | |||
disabled={isLoading} | |||
/> | |||
{errors[field.name] && ( | |||
<FormError>{errors[field.name].message}</FormError> | |||
<FormError>{errors[field.name]!.message}</FormError> |
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 why this is needed since we have the check errors[field.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.
Because, theoretically, the errors
object could have a getter (a computed property) implemented like this:
get someFieldName () {
return Math.random() > 0.5 ? undefined : { message: "You're lucky this time" }
}
In more precise terms, object field access is a function call and can easily be impure.
To get around this, you can define a local fieldError
variable above the JSX and then use that.
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, fun fact, my reasoning was wrong. TypeScript will happily allow this error to happen. Check it out.
Looks like TS specifically doesn't like if you do it twice (access a field of one object to get a string for accessing the field of another object). But my suggestion above still works. See here.
@@ -69,7 +69,7 @@ async function getAuthUserData(userId: {= userEntityUpper =}['id']): Promise<Aut | |||
throwInvalidCredentialsError() | |||
} | |||
|
|||
return createAuthUserData(user); | |||
return createAuthUserData(user!); |
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.
Again, I'm not sure why this is needed since we have the check if (!user) {
. Maybe it's not obvious to the compiler that the throwInvalidCredentialsError
fn throws?
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, I see this is taken care of now (because you're throwing explicitly.
Maybe it's not obvious to the compiler that the throwInvalidCredentialsError fn throws?
Yeah, that was it.
const { data: task, isLoading } = tasksCrud.get.useQuery({ | ||
id: parseInt(id, 10), | ||
}); | ||
const { id } = useParams<{ id: string }>() |
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.
Not related to this PR, but a quick fix since id
is of type string | undefined
.
waspc/data/Generator/templates/sdk/wasp/client/operations/queries/core.ts
Outdated
Show resolved
Hide resolved
@@ -1,9 +1,9 @@ | |||
{{={= =}=}} |
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've started to work on enabling strict null checks while working on env variables, so these are some changes I left from that effort - env
-> nodeEnv
since I imagined that the import of env vars would look like import { env } from 'wasp/server'
waspc/data/Generator/templates/sdk/wasp/client/operations/rpc.ts
Outdated
Show resolved
Hide resolved
18f9fd0
to
a509762
Compare
@@ -88,10 +88,10 @@ export function handleApiError(error: AxiosError<{ message?: string, data?: unkn | |||
// That would require copying HttpError code to web-app also and using it here. | |||
const responseJson = error.response?.data | |||
const responseStatusCode = error.response.status | |||
throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) | |||
return new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) |
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.
Why did we change this function?
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.
If we use handleApiError(error)
which throws the error in some fn - TS thinks that fn has undefined
as one of the return values.
If we do throw handleApiError(error)
in the same fn, TS now knows this throws and then the fn doesn't return i.e. it doesn't return undefined
.
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, yeah, saw the other comment that addresses that. Makes sense.
I can't believe I'm saying this, but this is why monads are nice (don't tell Martin).
You can resolve this when you read it.
const identities = Object.values(data.identities).filter(Boolean); | ||
return identities.length > 0 ? identities[0].id : null; | ||
return identities.length > 0 ? identities[0]!.id : null; |
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.
We should remove the assertion and change the filtering predicate:
const identities = Object.values(data.identities).filter(identity => identity !== null);
return identities.length > 0 ? identities[0].id : null;
Three benefits:
- TypeScript can now infer a type predicate so the
!
on the next line is no longer necessary. It's both safer and less fragile (the entire type procedure is localized and no longer spans two lines). identity => identity !== null
is cleaner thanBoolean
anyway. The first is clear to everyone, and the second only to those familiar with JS's quirks and idioms.
Note
Type predicate inference is a pretty new feature. It's available in TS 5.5, and our package.json
requires ^5.1.0
. This breaks the build for old users who have started their Wasp project before TS 5.5. was released.
We can either update TS to ^5.5
, or we can define an isNotNull
type guard as a named function and use that instead of a Boolean
. It behaves the same way but requires more ceremony.
The first option requires users to migrate stuff, while the second one doesn't. So it all depends on whether we want to release this as part of a minor or a major release.
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.
This breaks the build for old users who have started their Wasp project before TS 5.5. was released.
Are you sure it breaks the build if we just bump the TS version in package.json
? Shouldn't it just install a newer TS version for them?
EDIT: the users have their TS version defined in their package.json
. Yep. But we could introduce validation for the typescript
package as well, so we could force them to bump it :)
But, I think the isNotNull
solution is more in the scope of this 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.
Nice change. I would recommend merging the predicates
file with the types
file (in universal
). Seems like they belong together.
And no, I don't know how to call it 😄
(but you don't need to do anything, decide whatever you want and resolve the comment)
username: process.env.SMTP_USERNAME!, | ||
password: process.env.SMTP_PASSWORD!, |
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.
This file is throwing all kinds of errors for me. I tried to build the server with TypeScript (npx tsc
in .wasp/out/server
) and it failed.
Does this work for you normally (I'm guessing it does, because you wouldn't know these types needed changing if it didn't).
I probably messed something up because it also fails on older versions of main. If everything's normal for you, I'll dig in.
if (!authIdentity) { | ||
throw new Error(`User with email: ${email} not found.`); | ||
} |
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.
Will this cause a breaking change for users (Hyrum's law)?
We should fix it of course, just make sure to add a note to the changelog if it does. You can resolve the comment if and when you take care of it.
); | ||
} | ||
|
||
function serializeProviderData<PN extends ProviderName>(providerData: PossibleProviderData[PN]): string { | ||
return JSON.stringify(providerData); | ||
} | ||
|
||
async function sanitizeProviderData<PN extends ProviderName>( | ||
async function ensurePasswordIsHashed<PN extends ProviderName>( |
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.
Did we make an issue for fixing this in the end?
The field name for hashedPassword
doesn't allow for a correct type signature for this function and it can therefore be called multiple times?
If I got it right, we should rename password
to hashedPassword
where appropriate, create a new type, rename this function, and change it's type to be A -> B
instead of A -> A
?
): PossibleProviderData[PN] { | ||
// NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON. | ||
let data = JSON.parse(providerData) as PossibleProviderData[PN]; | ||
return JSON.parse(providerData) as PossibleProviderData[PN]; |
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.
You might not need the as
here. I think the function's return type already narrows unknown
to PossibleProviderData[PN]
.
@@ -261,34 +275,45 @@ export async function validateAndGetUserFields( | |||
} | |||
|
|||
// PUBLIC API | |||
export function deserializeAndSanitizeProviderData<PN extends ProviderName>( | |||
export function getProviderData<PN extends ProviderName>( |
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.
This was a breaking change, right? We should update the changelog and the public API table.
} | ||
|
||
// PUBLIC API | ||
export type CreateUserResult = {= userEntityUpper =} & { |
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.
Also a new addition it seems. We should:
- Update the public API table.
- Update the changelog.
@@ -302,7 +302,7 @@ function AdditionalFormFields({ | |||
disabled={isLoading} | |||
/> | |||
{errors[field.name] && ( | |||
<FormError>{errors[field.name].message}</FormError> | |||
<FormError>{errors[field.name]!.message}</FormError> |
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, fun fact, my reasoning was wrong. TypeScript will happily allow this error to happen. Check it out.
Looks like TS specifically doesn't like if you do it twice (access a field of one object to get a string for accessing the field of another object). But my suggestion above still works. See here.
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.
Nice! I'm approving.
Just make sure you cover these before merging:
- Enables strict null checks in SDK #2360 (comment)
- Enables strict null checks in SDK #2360 (comment)
- Enables strict null checks in SDK #2360 (comment)
- Enables strict null checks in SDK #2360 (comment)
- Enables strict null checks in SDK #2360 (comment)
I've fixed my part, left some comments explaining it, and gone through at your fixes.
I didn't run the e2e tests yet because you'll probably make a few more changes.
return null; | ||
} | ||
|
||
return result as FindAuthWithUserResult; |
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 preferrable options are:
const result = await prisma.auth.findFirst({ where, include: { user: true } });
return result && result.user && { ...result, user: result.user };
Or:
const result = await prisma.auth.findFirst({ where, include: { user: true } });
if (!result) {
return null;
}
const { user, ...auth } = result;
return user && { user, ...auth };
I prefer the second one.
// FIXME: query fns don't handle the `undefined` case correctly | ||
// https://github.com/wasp-lang/wasp/issues/2017 | ||
queryKey: makeQueryCacheKey(query, (queryFnArgs as Input)), | ||
// FIXME: query fns don't handle the `undefined` case correctly | ||
// https://github.com/wasp-lang/wasp/issues/2017 | ||
queryFn: () => query(queryFnArgs as Input), |
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.
Added the details into the issue: #2017 (comment)
@@ -138,7 +142,7 @@ type InternalOptimisticUpdateDefinition<ActionInput, CachedData> = { | |||
* the current state of the cache and returns the desired (new) state of the | |||
* cache. | |||
*/ | |||
type SpecificUpdateQuery<CachedData> = (oldData: CachedData) => CachedData; | |||
type SpecificUpdateQuery<CachedData> = (oldData: CachedData | undefined) => CachedData; |
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.
Also covered in #2017 (comment)
@@ -259,10 +263,10 @@ function makeRqOptimisticUpdateOptions<ActionInput, CachedData>( | |||
); | |||
|
|||
// We're using a Map to correctly serialize query keys that contain objects. | |||
const previousData = new Map(); | |||
const previousData: Map<QueryKey, CachedData | undefined> = new Map(); |
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.
Tl;dr everything about this part of the code is covered in #2017 (comment), but it turns out this was actually correct (but would be nicer if we handled it more explicitly).
@@ -71,9 +71,10 @@ export const toggleAllTasks: ToggleAllTasks = async (_args, context) => { | |||
throw new HttpError(401) | |||
} | |||
|
|||
const userId = context.user.id |
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.
Noice.
|
||
getJobScheduleData = | ||
maybe | ||
(object ["isDefined" .= False]) | ||
( \schedule -> | ||
object | ||
[ "isDefined" .= True, | ||
"cron" .= J.cron schedule, | ||
"args" .= getJobScheduleArgs (J.args schedule), | ||
"options" .= getJobSchduleOptions (J.scheduleExecutorOptionsJson job) | ||
] | ||
) | ||
getJobScheduleArgs = | ||
maybe | ||
(object ["isDefined" .= False]) | ||
(\args -> object ["isDefined" .= True, "json" .= Aeson.Text.encodeToLazyText args]) | ||
|
||
getJobSchduleOptions = | ||
maybe | ||
(object ["isDefined" .= False]) | ||
(\options -> object ["isDefined" .= True, "json" .= Aeson.Text.encodeToLazyText options]) | ||
|
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.
Looks great! Just please double check we didn't break anything (does the e2e test look alright to you).
We could add a few "complicated" job declarations to the job e2e test.
This PR enables
strictNullChecks
option in the SDKtsconfig.json
.We want to enable this option to make sure Zod schemas are working properly before we start using them for env variables validation.
Left to do
@ts-ignore
and a TODO for things that need more workUsing
@ts-ignore
is not that problematic since it will unblock us for using Zod schemas, but also explicitly mark parts of the code base that need some work. This work was still needed before this PR, but it was implicit.