Skip to content

Commit

Permalink
Enables strict null checks in SDK (#2360)
Browse files Browse the repository at this point in the history
  • Loading branch information
infomiho authored Nov 26, 2024
1 parent 26688e0 commit 75f0e80
Show file tree
Hide file tree
Showing 300 changed files with 1,864 additions and 1,111 deletions.
11 changes: 10 additions & 1 deletion waspc/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Next

### ⚠️ Breaking Changes

- Renamed and split `deserializeAndSanitizeProviderData` to `getProviderData` and `getProviderDataWithPassword` so it's more explicit if the resulting data will contain the hashed password or not.

### 🔧 Small improvements

- Enabled strict null checks for the Wasp SDK which means that some of the return types are more precise now e.g. you'll need to check if some value is `null` before using it.

## 0.15.2

### 🐞 Bug fixes
Expand Down Expand Up @@ -79,7 +89,6 @@ To learn more about this feature and how to activate it, check out the [Wasp TS
There are some breaking changes with React Router 6 which will require you to update your code.
Also, the new version of Prisma may cause breaking changes depending on how you're using it.


Read more about breaking changes in the migration guide: https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15 .

### 🐞 Bug fixes
Expand Down
6 changes: 3 additions & 3 deletions waspc/data/Generator/templates/sdk/wasp/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ window.addEventListener('storage', (event) => {
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void {
export function handleApiError<T extends AxiosError<{ message?: string, data?: unknown }>>(error: T): T | WaspHttpError {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
Expand All @@ -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)
} else {
// If any other error, we just propagate it.
throw error
return error
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export async function login(data: { email: string; password: string }): Promise<
const response = await api.post('{= loginPath =}', data);
await initSession(response.data.sessionId);
} catch (e) {
handleApiError(e);
throw handleApiError(e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function requestPasswordReset(data: { email: string; }): Promise<{
const response = await api.post('{= requestPasswordResetPath =}', data);
return response.data;
} catch (e) {
handleApiError(e);
throw handleApiError(e);
}
}

Expand All @@ -17,6 +17,6 @@ export async function resetPassword(data: { token: string; password: string; }):
const response = await api.post('{= resetPasswordPath =}', data);
return response.data;
} catch (e) {
handleApiError(e);
throw handleApiError(e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export async function signup(data: { email: string; password: string }): Promise
const response = await api.post('{= signupPath =}', data);
return response.data;
} catch (e) {
handleApiError(e);
throw handleApiError(e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export async function verifyEmail(data: {
const response = await api.post('{= verifyEmailPath =}', data)
return response.data
} catch (e) {
handleApiError(e)
throw handleApiError(e)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ function AdditionalFormFields({
}: {
hookForm: UseFormReturn<LoginSignupFormFields>;
formState: FormState;
additionalSignupFields: AdditionalSignupFields;
additionalSignupFields?: AdditionalSignupFields;
}) {
const {
register,
Expand All @@ -293,6 +293,7 @@ function AdditionalFormFields({
Component: any,
props?: React.ComponentProps<ComponentType>
) {
const errorMessage = errors[field.name]?.message;
return (
<FormItemGroup key={field.name}>
<FormLabel>{field.label}</FormLabel>
Expand All @@ -301,8 +302,8 @@ function AdditionalFormFields({
{...props}
disabled={isLoading}
/>
{errors[field.name] && (
<FormError>{errors[field.name].message}</FormError>
{errorMessage && (
<FormError>{errorMessage}</FormError>
)}
</FormItemGroup>
);
Expand Down Expand Up @@ -341,7 +342,7 @@ function isFieldRenderFn(
}

function areAdditionalFieldsRenderFn(
additionalSignupFields: AdditionalSignupFields
additionalSignupFields?: AdditionalSignupFields
): additionalSignupFields is AdditionalSignupFieldRenderFn {
return typeof additionalSignupFields === 'function'
}
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export default async function login(username: string, password: string): Promise

await initSession(response.data.sessionId)
} catch (error) {
handleApiError(error)
throw handleApiError(error)
}
}
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/sdk/wasp/auth/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type AuthUserData } from '../server/auth/user.js';

import { auth } from "./lucia.js";
import type { Session } from "lucia";
import { throwInvalidCredentialsError } from "./utils.js";
import { createInvalidCredentialsError } from "./utils.js";

import { prisma } from 'wasp/server';
import { createAuthUserData } from "../server/auth/user.js";
Expand Down Expand Up @@ -66,7 +66,7 @@ async function getAuthUserData(userId: {= userEntityUpper =}['id']): Promise<Aut
})

if (!user) {
throwInvalidCredentialsError()
throw createInvalidCredentialsError()
}

return createAuthUserData(user);
Expand Down
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/auth/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export default async function signup(userFields: { username: string; password: s
try {
await api.post('{= signupPath =}', userFields)
} catch (error) {
handleApiError(error)
throw handleApiError(error)
}
}
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function createUserGetter(): Query<void, AuthUser | null> {
if (error.response?.status === 401) {
return null
} else {
handleApiError(error)
throw handleApiError(error)
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions waspc/data/Generator/templates/sdk/wasp/auth/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
AuthUser,
UserEntityWithAuth,
} from '../server/auth/user.js'
import { isNotNull } from '../universal/predicates.js'

/**
* We split the user.ts code into two files to avoid some server-only
Expand Down Expand Up @@ -35,6 +36,7 @@ export type { AuthUserData, AuthUser } from '../server/auth/user.js'
// PRIVATE API (used in SDK and server)
export function makeAuthUserIfPossible(user: null): null
export function makeAuthUserIfPossible(user: AuthUserData): AuthUser
export function makeAuthUserIfPossible(user: AuthUserData | null): AuthUser | null
export function makeAuthUserIfPossible(
user: AuthUserData | null,
): AuthUser | null {
Expand All @@ -45,13 +47,13 @@ function makeAuthUser(data: AuthUserData): AuthUser {
return {
...data,
getFirstProviderUserId: () => {
const identities = Object.values(data.identities).filter(Boolean);
const identities = Object.values(data.identities).filter(isNotNull);
return identities.length > 0 ? identities[0].id : null;
},
};
}

function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): UserEntityWithAuth['auth']['identities'][number] | null {
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): NonNullable<UserEntityWithAuth['auth']>['identities'][number] | null {
if (!user.auth) {
return null;
}
Expand Down
61 changes: 43 additions & 18 deletions waspc/data/Generator/templates/sdk/wasp/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, user: result.user };
}

// PUBLIC API
export type CreateUserResult = {= userEntityUpper =} & {
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
Expand Down Expand Up @@ -261,34 +275,45 @@ export async function validateAndGetUserFields(
}

// PUBLIC API
export function deserializeAndSanitizeProviderData<PN extends ProviderName>(
export function getProviderData<PN extends ProviderName>(
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);
}

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>(
providerData: PossibleProviderData[PN],
): Promise<PossibleProviderData[PN]> {
const data = {
Expand All @@ -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 })
}
12 changes: 6 additions & 6 deletions waspc/data/Generator/templates/sdk/wasp/auth/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,37 @@ const EMAIL_FIELD = 'email';
const TOKEN_FIELD = 'token';

// PUBLIC API
export function ensureValidEmail(args: unknown): void {
export function ensureValidEmail(args: object): void {
validate(args, [
{ validates: EMAIL_FIELD, message: 'email must be present', validator: email => !!email },
{ validates: EMAIL_FIELD, message: 'email must be a valid email', validator: email => isValidEmail(email) },
]);
}

// PUBLIC API
export function ensureValidUsername(args: unknown): void {
export function ensureValidUsername(args: object): void {
validate(args, [
{ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }
]);
}

// PUBLIC API
export function ensurePasswordIsPresent(args: unknown): void {
export function ensurePasswordIsPresent(args: object): void {
validate(args, [
{ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password },
]);
}

// PUBLIC API
export function ensureValidPassword(args: unknown): void {
export function ensureValidPassword(args: object): void {
validate(args, [
{ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => isMinLength(password, 8) },
{ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => containsNumber(password) },
]);
}

// PUBLIC API
export function ensureTokenIsPresent(args: unknown): void {
export function ensureTokenIsPresent(args: object): void {
validate(args, [
{ validates: TOKEN_FIELD, message: 'token must be present', validator: token => !!token },
]);
Expand All @@ -47,7 +47,7 @@ export function throwValidationError(message: string): void {
throw new HttpError(422, 'Validation failed', { message })
}

function validate(args: unknown, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void {
function validate(args: object, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void {
for (const { validates, message, validator } of validators) {
if (!validator(args[validates])) {
throwValidationError(message);
Expand Down
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/client/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{={= =}=}}
import { stripTrailingSlash } from 'wasp/universal/url'
import { stripTrailingSlash } from '../universal/url.js'

const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || '{= defaultServerUrl =}';

Expand Down
Loading

0 comments on commit 75f0e80

Please sign in to comment.