WIP: Enables strict null checks in SDK

This commit is contained in:
Mihovil Ilakovac 2024-10-28 12:39:08 +01:00
parent c350cbe6e4
commit ddd10d1733
30 changed files with 97 additions and 65 deletions

View File

@ -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(error: AxiosError<{ message?: string, data?: unknown }>): AxiosError | WaspHttpError {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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>
)}
</FormItemGroup>
);
@ -341,7 +341,7 @@ function isFieldRenderFn(
}
function areAdditionalFieldsRenderFn(
additionalSignupFields: AdditionalSignupFields
additionalSignupFields?: AdditionalSignupFields
): additionalSignupFields is AdditionalSignupFieldRenderFn {
return typeof additionalSignupFields === 'function'
}

View File

@ -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)
}
}

View File

@ -69,7 +69,7 @@ async function getAuthUserData(userId: {= userEntityUpper =}['id']): Promise<Aut
throwInvalidCredentialsError()
}
return createAuthUserData(user);
return createAuthUserData(user!);
}
// PRIVATE API

View File

@ -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)
}
}

View File

@ -23,12 +23,14 @@ function createUserGetter(): Query<void, AuthUser | null> {
try {
const response = await api.get(getMeRoute.path)
const userData = superjsonDeserialize<AuthUserData | null>(response.data)
// TODO: figure out why overloading is not working
// @ts-ignore
return makeAuthUserIfPossible(userData)
} catch (error) {
if (error.response?.status === 401) {
return null
} else {
handleApiError(error)
throw handleApiError(error)
}
}
}

View File

@ -46,12 +46,12 @@ function makeAuthUser(data: AuthUserData): AuthUser {
...data,
getFirstProviderUserId: () => {
const identities = Object.values(data.identities).filter(Boolean);
return identities.length > 0 ? identities[0].id : null;
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;
}

View File

@ -125,13 +125,13 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
}
type FindAuthWithUserResult = {= authEntityUpper =} & {
{= userFieldOnAuthEntityName =}: {= userEntityUpper =}
{= userFieldOnAuthEntityName =}: {= userEntityUpper =} | null
}
// PRIVATE API
export async function findAuthWithUserBy(
where: Prisma.{= authEntityUpper =}WhereInput
): Promise<FindAuthWithUserResult> {
): Promise<FindAuthWithUserResult | null> {
return prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }});
}
@ -141,7 +141,7 @@ export async function createUser(
serializedProviderData?: string,
userFields?: PossibleUserFields,
): Promise<{= userEntityUpper =} & {
auth: {= authEntityUpper =}
auth: {= authEntityUpper =} | null
}> {
return prisma.{= userEntityLower =}.create({
data: {
@ -269,6 +269,8 @@ export function deserializeAndSanitizeProviderData<PN extends ProviderName>(
let data = JSON.parse(providerData) as PossibleProviderData[PN];
if (providerDataHasPasswordField(data) && shouldRemovePasswordField) {
// TODO: fix this type
// @ts-ignore
delete data.hashedPassword;
}

View File

@ -6,7 +6,7 @@ const EMAIL_FIELD = 'email';
const TOKEN_FIELD = 'token';
// PUBLIC API
export function ensureValidEmail(args: unknown): void {
export function ensureValidEmail<Args extends object>(args: Args): 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) },
@ -14,21 +14,21 @@ export function ensureValidEmail(args: unknown): void {
}
// PUBLIC API
export function ensureValidUsername(args: unknown): void {
export function ensureValidUsername<Args extends object>(args: Args): 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 extends object>(args: Args): 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 extends object>(args: Args): 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) },
@ -36,7 +36,7 @@ export function ensureValidPassword(args: unknown): void {
}
// PUBLIC API
export function ensureTokenIsPresent(args: unknown): void {
export function ensureTokenIsPresent<Args extends object>(args: Args): void {
validate(args, [
{ validates: TOKEN_FIELD, message: 'token must be present', validator: token => !!token },
]);
@ -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 extends object>(args: Args, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void {
for (const { validates, message, validator } of validators) {
if (!validator(args[validates])) {
throwValidationError(message);

View File

@ -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 =}';

View File

@ -170,7 +170,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.");
}
@ -207,9 +207,11 @@ function makeOptimisticUpdateMutationFn<Input, Output, CachedData>(
return (function performActionWithOptimisticUpdates(item?: Input) {
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map(
(generalDefinition) =>
// @ts-ignore
getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item)
);
return (actionFn as InternalAction<Input, Output>).internal(
// @ts-ignore
item,
specificOptimisticUpdateDefinitions
);
@ -262,11 +264,13 @@ function makeRqOptimisticUpdateOptions<ActionInput, CachedData>(
const previousData = new Map();
specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => {
// Snapshot the currently cached value.
// @ts-ignore
const previousDataForQuery: CachedData =
queryClient.getQueryData(queryKey);
// Attempt to optimistically update the cache using the new value.
try {
// @ts-ignore
queryClient.setQueryData(queryKey, updateQuery);
} catch (e) {
console.error(

View File

@ -15,7 +15,7 @@ export async function callOperation(operationRoute: OperationRoute, args: any) {
const response = await api.post(operationRoute.path, superjsonArgs)
return superjsonDeserialize(response.data)
} catch (error) {
handleApiError(error)
throw handleApiError(error)
}
}

View File

@ -18,7 +18,7 @@ import {
// Details here: https://github.com/wasp-lang/wasp/issues/2017
export function makeQueryCacheKey<Input, Output>(
query: Query<Input, Output>,
payload: Input
payload?: Input
): (string | Input)[] {
return payload !== undefined ?
[...query.queryCacheKey, payload]

View File

@ -93,4 +93,5 @@ type ClientOperationWithNonAnyInput<Input, Output> =
? (args?: unknown) => Promise<Output>
: [Input] extends [void]
? () => Promise<Output>
: (args: Input) => Promise<Output>
// TODO: decide if this is what we want?
: (args?: Input) => Promise<Output>

View File

@ -31,8 +31,8 @@ const auth = handleRejection(async (req, res, next) => {
throwInvalidCredentialsError()
}
req.sessionId = sessionAndUser.session.id
req.user = sessionAndUser.user
req.sessionId = sessionAndUser!.session.id
req.user = sessionAndUser!.user
next()
})

View File

@ -2,7 +2,7 @@ export class HttpError extends Error {
public statusCode: number
public data: unknown
constructor (statusCode: number, message?: string, data?: Record<string, unknown>, ...params: unknown[]) {
constructor (statusCode: number, message?: string, data?: Record<string, unknown>, ...params: any[]) {
super(message, ...params)
if (Error.captureStackTrace) {

View File

@ -64,7 +64,13 @@ async function sendEmailAndSaveMetadata(
// so the user can't send multiple requests while the email is being sent.
const providerId = createProviderId("email", email);
const authIdentity = await findAuthIdentity(providerId);
const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData);
if (!authIdentity) {
throw new Error(`User with email: ${email} not found.`);
}
const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity!.providerData);
await updateAuthIdentityProviderData<'email'>(providerId, providerData, metadata);
emailSender.send(content).catch((e) => {

View File

@ -100,7 +100,7 @@ type OnBeforeLoginHookParams = {
/**
* User that is trying to log in.
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user']
user: NonNullable<Awaited<ReturnType<typeof findAuthWithUserBy>>>['user']
/**
* Request object that can be used to access the incoming request.
*/
@ -115,7 +115,7 @@ type OnAfterLoginHookParams = {
/**
* User that is logged in.
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user']
user: NonNullable<Awaited<ReturnType<typeof findAuthWithUserBy>>>['user']
/**
* OAuth flow data that was generated during the OAuth flow. This is only
* available if the user logged in using OAuth.

View File

@ -39,5 +39,5 @@ function getRedirectUriForError(error: string): URL {
}
function isHttpErrorWithExtraMessage(error: HttpError): error is HttpError & { data: { message: string } } {
return error.data && typeof (error.data as any).message === 'string';
return error.data ? typeof (error.data as any).message === 'string' : false;
}

View File

@ -1,9 +1,9 @@
{{={= =}=}}
import merge from 'lodash.merge'
import { stripTrailingSlash } from "wasp/universal/url";
import { stripTrailingSlash } from "../universal/url.js";
const env = process.env.NODE_ENV || 'development'
const nodeEnv = process.env.NODE_ENV ?? 'development'
// TODO:
// - Use dotenv library to consume env vars from a file.
@ -39,9 +39,9 @@ const config: {
production: EnvConfig,
} = {
all: {
env,
isDevelopment: env === 'development',
port: parseInt(process.env.PORT) || {= defaultServerPort =},
env: nodeEnv,
isDevelopment: nodeEnv === 'development',
port: process.env.PORT ? parseInt(process.env.PORT) : {= defaultServerPort =},
databaseUrl: process.env.{= databaseUrlEnvVarName =},
allowedCORSOrigins: [],
{=# isAuthEnabled =}
@ -54,15 +54,17 @@ const config: {
production: getProductionConfig(),
}
const resolvedConfig: Config = merge(config.all, config[env])
const resolvedConfig: Config = merge(config.all, config[nodeEnv])
// PUBLIC API
export default resolvedConfig
function getDevelopmentConfig(): EnvConfig {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || '{= defaultClientUrl =}');
const serverUrl = stripTrailingSlash(process.env.WASP_SERVER_URL || '{= defaultServerUrl =}');
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) ?? '{= defaultClientUrl =}';
const serverUrl = stripTrailingSlash(process.env.WASP_SERVER_URL) ?? '{= defaultServerUrl =}';
return {
// @ts-ignore
frontendUrl,
// @ts-ignore
serverUrl,
allowedCORSOrigins: '*',
{=# isAuthEnabled =}
@ -77,8 +79,11 @@ function getProductionConfig(): EnvConfig {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL);
const serverUrl = stripTrailingSlash(process.env.WASP_SERVER_URL);
return {
// @ts-ignore
frontendUrl,
// @ts-ignore
serverUrl,
// @ts-ignore
allowedCORSOrigins: [frontendUrl],
{=# isAuthEnabled =}
auth: {

View File

@ -5,10 +5,12 @@ import { EmailSender } from "./core/types.js";
{=# isSmtpProviderUsed =}
const emailProvider = {
type: "smtp",
host: process.env.SMTP_HOST,
// TODO: We'll validate this
host: process.env.SMTP_HOST!,
// @ts-ignore
port: parseInt(process.env.SMTP_PORT, 10),
username: process.env.SMTP_USERNAME,
password: process.env.SMTP_PASSWORD,
username: process.env.SMTP_USERNAME!,
password: process.env.SMTP_PASSWORD!,
} as const;
{=/ isSmtpProviderUsed =}
{=# isSendGridProviderUsed =}

View File

@ -16,6 +16,8 @@ export type {= typeName =}<Input extends JSONObject, Output extends JSONValue |
export const {= jobName =} = createJobDefinition({
jobName: '{= jobName =}',
defaultJobOptions: {=& jobPerformOptions =},
// TODO: output job schedule args as undefined if not provided
// @ts-ignore
jobSchedule: {=& jobSchedule =},
entities,
})

View File

@ -39,6 +39,7 @@ export function createJobDefinition<
}) {
return new PgBossJob<Input, Output, Entities>(
jobName,
// @ts-ignore
defaultJobOptions,
entities,
jobSchedule,
@ -90,7 +91,7 @@ export function registerJob<
await boss.schedule(
job.jobName,
job.jobSchedule.cron,
job.jobSchedule.args || null,
job.jobSchedule.args,
options
)
}
@ -107,8 +108,8 @@ class PgBossJob<
Output extends JSONValue | void,
Entities extends Partial<PrismaDelegate>
> extends Job {
public readonly defaultJobOptions: Parameters<PgBoss['send']>[2]
public readonly startAfter: number | string | Date
public readonly defaultJobOptions?: Parameters<PgBoss['send']>[2]
public readonly startAfter: number | string | Date | undefined
public readonly entities: Entities
public readonly jobSchedule: JobSchedule | null
@ -128,6 +129,7 @@ class PgBossJob<
delay(startAfter: number | string | Date) {
return new PgBossJob<Input, Output, Entities>(
this.jobName,
// @ts-ignore
this.defaultJobOptions,
this.entities,
this.jobSchedule,

View File

@ -14,11 +14,11 @@
"alwaysStrict": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
// See https://github.com/wasp-lang/wasp/issues/2056 before activating this:
// "useUnknownInCatchVariables": true,
// The following 3 stict options will require more work:
// "noImplicitAny": true,
// "strictNullChecks": true,
// "strictPropertyInitialization": true,
// Overriding this because we want to use top-level await
"module": "esnext",

View File

@ -1,15 +1,20 @@
import "./Main.css";
import React from "react";
import { useParams } from "react-router-dom";
import { Link } from "wasp/client/router";
import './Main.css'
import React from 'react'
import { useParams } from 'react-router-dom'
import { Link } from 'wasp/client/router'
import { tasks as tasksCrud } from "wasp/client/crud";
import { tasks as tasksCrud } from 'wasp/client/crud'
const DetailPage = () => {
const { id } = useParams<{ id: string }>();
const { data: task, isLoading } = tasksCrud.get.useQuery({
id: parseInt(id, 10),
});
const { id } = useParams<{ id: string }>()
const { data: task, isLoading } = tasksCrud.get.useQuery(
{
id: parseInt(id!, 10),
},
{
enabled: !!id,
}
)
return (
<div className="container">
@ -30,7 +35,7 @@ const DetailPage = () => {
<Link to="/">Return</Link>
</main>
</div>
);
};
)
}
export default DetailPage;
export default DetailPage

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import { AuthUser } from 'wasp/auth'
import { getMe } from 'wasp/client/auth'
import {