-
-
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?
Changes from all commits
ddd10d1
90baa94
b5c3c63
7afc4f0
b4e2362
465856c
5c329a4
e04aac3
1928b6f
e231dda
f431153
c8b2120
08304fe
a509762
4a88a94
7347058
7aaa7b4
ef74f89
ccb56b8
2e74b57
0df79f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -280,7 +280,7 @@ function AdditionalFormFields({ | |
}: { | ||
hookForm: UseFormReturn<LoginSignupFormFields>; | ||
formState: FormState; | ||
additionalSignupFields: AdditionalSignupFields; | ||
additionalSignupFields?: AdditionalSignupFields; | ||
}) { | ||
const { | ||
register, | ||
|
@@ -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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because, theoretically, the 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
)} | ||
</FormItemGroup> | ||
); | ||
|
@@ -341,7 +341,7 @@ function isFieldRenderFn( | |
} | ||
|
||
function areAdditionalFieldsRenderFn( | ||
additionalSignupFields: AdditionalSignupFields | ||
additionalSignupFields?: AdditionalSignupFields | ||
): additionalSignupFields is AdditionalSignupFieldRenderFn { | ||
return typeof additionalSignupFields === 'function' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,7 +110,7 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>( | |
): Promise<{= authIdentityEntityUpper =}> { | ||
// We are doing the sanitization here only on updates to avoid | ||
// hashing the password multiple times. | ||
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates); | ||
const sanitizedProviderDataUpdates = await ensurePasswordIsHashed(providerDataUpdates); | ||
const newProviderData = { | ||
...existingProviderData, | ||
...sanitizedProviderDataUpdates, | ||
|
@@ -124,25 +124,39 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>( | |
}); | ||
} | ||
|
||
type FindAuthWithUserResult = {= authEntityUpper =} & { | ||
// PRIVATE API | ||
export type FindAuthWithUserResult = {= authEntityUpper =} & { | ||
{= userFieldOnAuthEntityName =}: {= userEntityUpper =} | ||
} | ||
|
||
// PRIVATE API | ||
export async function findAuthWithUserBy( | ||
where: Prisma.{= authEntityUpper =}WhereInput | ||
): Promise<FindAuthWithUserResult> { | ||
return prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }}); | ||
): Promise<FindAuthWithUserResult | null> { | ||
const result = await prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }}); | ||
|
||
if (result === null) { | ||
return null; | ||
} | ||
|
||
if (result.user === null) { | ||
return null; | ||
} | ||
|
||
return result as FindAuthWithUserResult; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
|
||
// PUBLIC API | ||
export type CreateUserResult = {= userEntityUpper =} & { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also a new addition it seems. We should:
|
||
auth: {= authEntityUpper =} | null | ||
} | ||
|
||
// PUBLIC API | ||
export async function createUser( | ||
providerId: ProviderId, | ||
serializedProviderData?: string, | ||
userFields?: PossibleUserFields, | ||
): Promise<{= userEntityUpper =} & { | ||
auth: {= authEntityUpper =} | ||
}> { | ||
): Promise<CreateUserResult> { | ||
return prisma.{= userEntityLower =}.create({ | ||
data: { | ||
// Using any here to prevent type errors when userFields are not | ||
|
@@ -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 commentThe 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. |
||
providerData: string, | ||
): Omit<PossibleProviderData[PN], 'hashedPassword'> { | ||
return sanitizeProviderData(getProviderDataWithPassword(providerData)); | ||
} | ||
|
||
// PUBLIC API | ||
export function getProviderDataWithPassword<PN extends ProviderName>( | ||
providerData: string, | ||
{ shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {}, | ||
): 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 commentThe reason will be displayed to describe this comment to others. Learn more. You might not need the |
||
} | ||
|
||
if (providerDataHasPasswordField(data) && shouldRemovePasswordField) { | ||
delete data.hashedPassword; | ||
function sanitizeProviderData<PN extends ProviderName>( | ||
providerData: PossibleProviderData[PN], | ||
): Omit<PossibleProviderData[PN], 'hashedPassword'> { | ||
if (providerDataHasPasswordField(providerData)) { | ||
const { hashedPassword, ...rest } = providerData; | ||
return rest; | ||
} else { | ||
return providerData; | ||
} | ||
|
||
return data; | ||
} | ||
|
||
// PUBLIC API | ||
export async function sanitizeAndSerializeProviderData<PN extends ProviderName>( | ||
providerData: PossibleProviderData[PN], | ||
): Promise<string> { | ||
return serializeProviderData( | ||
await sanitizeProviderData(providerData) | ||
await ensurePasswordIsHashed(providerData) | ||
); | ||
} | ||
|
||
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 commentThe 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 If I got it right, we should rename |
||
providerData: PossibleProviderData[PN], | ||
): Promise<PossibleProviderData[PN]> { | ||
const data = { | ||
|
@@ -309,6 +334,6 @@ function providerDataHasPasswordField( | |
} | ||
|
||
// PRIVATE API | ||
export function throwInvalidCredentialsError(message?: string): void { | ||
throw new HttpError(401, 'Invalid credentials', { message }) | ||
export function createInvalidCredentialsError(message?: string): HttpError { | ||
return new HttpError(401, 'Invalid credentials', { message }) | ||
sodic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,8 +28,12 @@ export function useQuery<Input, Output>( | |
return rqUseQuery({ | ||
// todo: The full queryCacheKey is constructed in two places, both here and | ||
// inside the Query. See https://github.com/wasp-lang/wasp/issues/2017 | ||
queryKey: makeQueryCacheKey(query, queryFnArgs), | ||
queryFn: () => query(queryFnArgs), | ||
// 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), | ||
Comment on lines
+31
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the details into the issue: #2017 (comment) |
||
...options, | ||
}) | ||
} | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Also covered in #2017 (comment) |
||
|
||
/** | ||
* A specific, "instantiated" optimistic update definition which contains a | ||
|
@@ -170,7 +174,7 @@ function translateToInternalDefinition<Item, CachedData>( | |
): InternalOptimisticUpdateDefinition<Item, CachedData> { | ||
const { getQuerySpecifier, updateQuery } = publicOptimisticUpdateDefinition; | ||
|
||
const definitionErrors = []; | ||
const definitionErrors: string[] = []; | ||
if (typeof getQuerySpecifier !== "function") { | ||
definitionErrors.push("`getQuerySpecifier` is not a function."); | ||
} | ||
|
@@ -204,7 +208,7 @@ function makeOptimisticUpdateMutationFn<Input, Output, CachedData>( | |
CachedData | ||
>[] | ||
): typeof actionFn { | ||
return (function performActionWithOptimisticUpdates(item?: Input) { | ||
return (function performActionWithOptimisticUpdates(item: Input) { | ||
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map( | ||
(generalDefinition) => | ||
getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item) | ||
|
@@ -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 commentThe 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). |
||
specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => { | ||
// Snapshot the currently cached value. | ||
const previousDataForQuery: CachedData = | ||
const previousDataForQuery: CachedData | undefined = | ||
queryClient.getQueryData(queryKey); | ||
|
||
// Attempt to optimistically update the cache using the new value. | ||
|
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 hasundefined
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 returnundefined
.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.