From ae3782bd153c9ec2826a6130f5d40af34dc0bcf7 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Thu, 20 Feb 2025 22:22:38 +0100 Subject: [PATCH 1/9] Adds runtime validation for operation inputs. --- template/app/src/demo-ai-app/operations.ts | 64 ++++++++++++++---- .../app/src/file-upload/FileUploadPage.tsx | 18 ++--- template/app/src/file-upload/fileUploading.ts | 66 ++++++++++++------- template/app/src/file-upload/operations.ts | 42 ++++++++---- template/app/src/file-upload/s3Utils.ts | 42 ++++++------ template/app/src/file-upload/validation.ts | 9 +++ template/app/src/payment/operations.ts | 17 +++-- template/app/src/payment/plans.ts | 11 +++- template/app/src/server/validation.ts | 14 ++++ template/app/src/user/operations.ts | 57 ++++++++++------ 10 files changed, 233 insertions(+), 107 deletions(-) create mode 100644 template/app/src/file-upload/validation.ts create mode 100644 template/app/src/server/validation.ts diff --git a/template/app/src/demo-ai-app/operations.ts b/template/app/src/demo-ai-app/operations.ts index ca58426..d3bd4a3 100644 --- a/template/app/src/demo-ai-app/operations.ts +++ b/template/app/src/demo-ai-app/operations.ts @@ -1,3 +1,4 @@ +import * as z from 'zod'; import type { Task, GptResponse } from 'wasp/entities'; import type { GenerateGptResponse, @@ -10,6 +11,7 @@ import type { import { HttpError } from 'wasp/server'; import { GeneratedSchedule } from './schedule'; import OpenAI from 'openai'; +import { ensureArgsSchemaOrThrowHttpError } from '../server/validation'; const openai = setupOpenAI(); function setupOpenAI() { @@ -20,15 +22,23 @@ function setupOpenAI() { } //#region Actions -type GptPayload = { - hours: string; -}; -export const generateGptResponse: GenerateGptResponse = async ({ hours }, context) => { +const generateGptResponseInputSchema = z.object({ + hours: z.string().regex(/^\d+(\.\d+)?$/, 'Hours must be a number'), +}); + +type GenerateGptResponseInput = z.infer; + +export const generateGptResponse: GenerateGptResponse = async ( + rawArgs: unknown, + context +) => { if (!context.user) { throw new HttpError(401); } + const args = ensureArgsSchemaOrThrowHttpError(generateGptResponseInputSchema, rawArgs); + const tasks = await context.entities.Task.findMany({ where: { user: { @@ -79,7 +89,9 @@ export const generateGptResponse: GenerateGptResponse, Task> = async ({ description }, context) => { +const createTaskInputSchema = z.object({ + description: z.string().nonempty(), +}); + +type CreateTaskInput = z.infer; + +export const createTask: CreateTask = async (rawArgs: unknown, context) => { if (!context.user) { throw new HttpError(401); } + const args = ensureArgsSchemaOrThrowHttpError(createTaskInputSchema, rawArgs); + const task = await context.entities.Task.create({ data: { - description, + description: args.description, user: { connect: { id: context.user.id } }, }, }); @@ -196,32 +216,50 @@ export const createTask: CreateTask, Task> = async ({ return task; }; -export const updateTask: UpdateTask, Task> = async ({ id, isDone, time }, context) => { +const updateTaskInputSchema = z.object({ + id: z.string().nonempty(), + isDone: z.boolean().optional(), + time: z.string().optional(), +}); + +type UpdateTaskInput = z.infer; + +export const updateTask: UpdateTask = async (rawArgs: unknown, context) => { if (!context.user) { throw new HttpError(401); } + const args = ensureArgsSchemaOrThrowHttpError(updateTaskInputSchema, rawArgs); + const task = await context.entities.Task.update({ where: { - id, + id: args.id, }, data: { - isDone, - time, + isDone: args.isDone, + time: args.time, }, }); return task; }; -export const deleteTask: DeleteTask, Task> = async ({ id }, context) => { +const deleteTaskInputSchema = z.object({ + id: z.string().nonempty(), +}); + +type DeleteTaskInput = z.infer; + +export const deleteTask: DeleteTask = async (rawArgs: unknown, context) => { if (!context.user) { throw new HttpError(401); } + const args = ensureArgsSchemaOrThrowHttpError(deleteTaskInputSchema, rawArgs); + const task = await context.entities.Task.delete({ where: { - id, + id: args.id, }, }); diff --git a/template/app/src/file-upload/FileUploadPage.tsx b/template/app/src/file-upload/FileUploadPage.tsx index 74c7145..2c42531 100644 --- a/template/app/src/file-upload/FileUploadPage.tsx +++ b/template/app/src/file-upload/FileUploadPage.tsx @@ -2,7 +2,8 @@ import { cn } from '../client/cn'; import { useState, useEffect, FormEvent } from 'react'; import type { File } from 'wasp/entities'; import { useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations'; -import { type FileUploadError, uploadFileWithProgress, validateFile, ALLOWED_FILE_TYPES } from './fileUploading'; +import { type FileUploadError, parseValidFile, uploadFileWithProgress } from './fileUploading'; +import { ALLOWED_FILE_TYPES } from './validation'; export default function FileUploadPage() { const [fileKeyForS3, setFileKeyForS3] = useState(''); @@ -10,7 +11,7 @@ export default function FileUploadPage() { const [uploadError, setUploadError] = useState(null); const allUserFiles = useQuery(getAllFilesByUser, undefined, { - // We disable automatic refetching because otherwise files would be refetched after `createFile` is called and the S3 URL is returned, + // We disable automatic refetching because otherwise files would be refetched after `createFile` is called and the S3 URL is returned, // which happens before the file is actually fully uploaded. Instead, we manually (re)fetch on mount and after the upload is complete. enabled: false, }); @@ -64,13 +65,12 @@ export default function FileUploadPage() { return; } - const validationError = validateFile(file); - if (validationError) { - setUploadError(validationError); + const validFileResult = parseValidFile(file); + if (validFileResult.kind === 'error') { + setUploadError(validFileResult.error); return; } - - await uploadFileWithProgress({ file, setUploadProgressPercent }); + await uploadFileWithProgress({ file: validFileResult.file, setUploadProgressPercent }); formElement.reset(); allUserFiles.refetch(); } catch (error) { @@ -117,11 +117,11 @@ export default function FileUploadPage() { <> Uploading {uploadProgressPercent}%
diff --git a/template/app/src/file-upload/fileUploading.ts b/template/app/src/file-upload/fileUploading.ts index 50ec5d5..9c2ebe2 100644 --- a/template/app/src/file-upload/fileUploading.ts +++ b/template/app/src/file-upload/fileUploading.ts @@ -1,30 +1,16 @@ import { Dispatch, SetStateAction } from 'react'; import { createFile } from 'wasp/client/operations'; import axios from 'axios'; +import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from './validation'; interface FileUploadProgress { - file: File; + file: FileWithValidType; setUploadProgressPercent: Dispatch>; } -export interface FileUploadError { - message: string; - code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED'; -} - -export const MAX_FILE_SIZE = 5 * 1024 * 1024; // Set this to the max file size you want to allow (currently 5MB). -export const ALLOWED_FILE_TYPES = [ - 'image/jpeg', - 'image/png', - 'application/pdf', - 'text/*', - 'video/quicktime', - 'video/mp4', -]; - export async function uploadFileWithProgress({ file, setUploadProgressPercent }: FileUploadProgress) { - const { uploadUrl } = await createFile({ fileType: file.type, name: file.name }); - return await axios.put(uploadUrl, file, { + const { uploadUrl } = await createFile({ fileType: file.type, fileName: file.name }); + return axios.put(uploadUrl, file, { headers: { 'Content-Type': file.type, }, @@ -37,18 +23,48 @@ export async function uploadFileWithProgress({ file, setUploadProgressPercent }: }); } -export function validateFile(file: File): FileUploadError | null { +export interface FileUploadError { + message: string; + code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED'; +} + +type AllowedFileType = (typeof ALLOWED_FILE_TYPES)[number]; +type FileWithValidType = Omit & { type: AllowedFileType }; + +type FileParseResult = + | { kind: 'success'; file: FileWithValidType } + | { + kind: 'error'; + error: { message: string; code: 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' }; + }; + +export function parseValidFile(file: File): FileParseResult { if (file.size > MAX_FILE_SIZE) { return { - message: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`, - code: 'FILE_TOO_LARGE', + kind: 'error', + error: { + message: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`, + code: 'FILE_TOO_LARGE', + }, }; } - if (!ALLOWED_FILE_TYPES.includes(file.type)) { + + if (!isAllowedFileType(file.type)) { return { - message: `File type '${file.type}' is not supported.`, - code: 'INVALID_FILE_TYPE', + kind: 'error', + error: { + message: `File type '${file.type}' is not supported.`, + code: 'INVALID_FILE_TYPE', + }, }; } - return null; + + return { + kind: 'success', + file: file as FileWithValidType, + }; +} + +function isAllowedFileType(fileType: string): fileType is AllowedFileType { + return (ALLOWED_FILE_TYPES as readonly string[]).includes(fileType); } diff --git a/template/app/src/file-upload/operations.ts b/template/app/src/file-upload/operations.ts index 59ab513..6b93d9a 100644 --- a/template/app/src/file-upload/operations.ts +++ b/template/app/src/file-upload/operations.ts @@ -1,3 +1,4 @@ +import * as z from 'zod'; import { HttpError } from 'wasp/server'; import { type File } from 'wasp/entities'; import { @@ -7,27 +8,35 @@ import { } from 'wasp/server/operations'; import { getUploadFileSignedURLFromS3, getDownloadFileSignedURLFromS3 } from './s3Utils'; +import { ensureArgsSchemaOrThrowHttpError } from '../server/validation'; +import { ALLOWED_FILE_TYPES } from './validation'; -type FileDescription = { - fileType: string; - name: string; -}; +const createFileInputSchema = z.object({ + fileType: z.enum(ALLOWED_FILE_TYPES), + fileName: z.string().nonempty(), +}); -export const createFile: CreateFile = async ({ fileType, name }, context) => { +type CreateFileInput = z.infer; + +export const createFile: CreateFile = async (rawArgs: unknown, context) => { if (!context.user) { throw new HttpError(401); } - const userInfo = context.user.id; + const args = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs); - const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo }); + const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ + fileType: args.fileType, + fileName: args.fileName, + userId: context.user.id, + }); return await context.entities.File.create({ data: { - name, + name: args.fileName, key, uploadUrl, - type: fileType, + type: args.fileType, user: { connect: { id: context.user.id } }, }, }); @@ -49,9 +58,14 @@ export const getAllFilesByUser: GetAllFilesByUser = async (_args, }); }; -export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async ( - { key }, - _context -) => { - return await getDownloadFileSignedURLFromS3({ key }); +const getDownloadFileSignedURLInputSchema = z.object({ key: z.string().nonempty() }); + +type GetDownloadFileSignedURLInput = z.infer; + +export const getDownloadFileSignedURL: GetDownloadFileSignedURL< + GetDownloadFileSignedURLInput, + string +> = async (rawArgs: unknown, _context) => { + const args = ensureArgsSchemaOrThrowHttpError(getDownloadFileSignedURLInputSchema, rawArgs); + return await getDownloadFileSignedURLFromS3({ key: args.key }); }; diff --git a/template/app/src/file-upload/s3Utils.ts b/template/app/src/file-upload/s3Utils.ts index 709f805..6fbbdf4 100644 --- a/template/app/src/file-upload/s3Utils.ts +++ b/template/app/src/file-upload/s3Utils.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { randomUUID } from 'crypto'; import { S3Client } from '@aws-sdk/client-s3'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; @@ -13,27 +14,30 @@ const s3Client = new S3Client({ type S3Upload = { fileType: string; - userInfo: string; -} + fileName: string; + userId: string; +}; -export const getUploadFileSignedURLFromS3 = async ({fileType, userInfo}: S3Upload) => { - const ex = fileType.split('/')[1]; - const Key = `${userInfo}/${randomUUID()}.${ex}`; - const s3Params = { - Bucket: process.env.AWS_S3_FILES_BUCKET, - Key, - ContentType: `${fileType}`, - }; - const command = new PutObjectCommand(s3Params); - const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600,}); - return { uploadUrl, key: Key }; -} - -export const getDownloadFileSignedURLFromS3 = async ({ key }: { key: string }) => { - const s3Params = { +export const getUploadFileSignedURLFromS3 = async ({ fileName, fileType, userId }: S3Upload) => { + const key = getS3Key(fileName, userId); + const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_FILES_BUCKET, Key: key, - }; - const command = new GetObjectCommand(s3Params); + ContentType: fileType, + }); + const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); + return { uploadUrl, key }; +}; + +export const getDownloadFileSignedURLFromS3 = async ({ key }: { key: string }) => { + const command = new GetObjectCommand({ + Bucket: process.env.AWS_S3_FILES_BUCKET, + Key: key, + }); return await getSignedUrl(s3Client, command, { expiresIn: 3600 }); +}; + +function getS3Key(fileName: string, userId: string) { + const ext = path.extname(fileName).slice(1); + return `${userId}/${randomUUID()}.${ext}`; } diff --git a/template/app/src/file-upload/validation.ts b/template/app/src/file-upload/validation.ts new file mode 100644 index 0000000..eb5750e --- /dev/null +++ b/template/app/src/file-upload/validation.ts @@ -0,0 +1,9 @@ +export const MAX_FILE_SIZE = 5 * 1024 * 1024; // Set this to the max file size you want to allow (currently 5MB). +export const ALLOWED_FILE_TYPES = [ + 'image/jpeg', + 'image/png', + 'application/pdf', + 'text/*', + 'video/quicktime', + 'video/mp4', +] as const; diff --git a/template/app/src/payment/operations.ts b/template/app/src/payment/operations.ts index 86b833c..8873468 100644 --- a/template/app/src/payment/operations.ts +++ b/template/app/src/payment/operations.ts @@ -1,20 +1,27 @@ +import * as z from 'zod'; import type { GenerateCheckoutSession, GetCustomerPortalUrl } from 'wasp/server/operations'; import { PaymentPlanId, paymentPlans } from '../payment/plans'; import { paymentProcessor } from './paymentProcessor'; import { HttpError } from 'wasp/server'; +import { ensureArgsSchemaOrThrowHttpError } from '../server/validation'; export type CheckoutSession = { sessionUrl: string | null; sessionId: string; }; -export const generateCheckoutSession: GenerateCheckoutSession = async ( - paymentPlanId, - context -) => { +const generateCheckoutSessionSchema = z.nativeEnum(PaymentPlanId); + +type GenerateCheckoutSessionInput = z.infer; + +export const generateCheckoutSession: GenerateCheckoutSession< + GenerateCheckoutSessionInput, + CheckoutSession +> = async (rawPaymentPlanId, context) => { if (!context.user) { throw new HttpError(401); } + const paymentPlanId = ensureArgsSchemaOrThrowHttpError(generateCheckoutSessionSchema, rawPaymentPlanId); const userId = context.user.id; const userEmail = context.user.email; if (!userEmail) { @@ -29,7 +36,7 @@ export const generateCheckoutSession: GenerateCheckoutSession; export enum PaymentPlanId { Hobby = 'hobby', @@ -9,7 +16,7 @@ export enum PaymentPlanId { } export interface PaymentPlan { - // Returns the id under which this payment plan is identified on your payment processor. + // Returns the id under which this payment plan is identified on your payment processor. // E.g. this might be price id on Stripe, or variant id on LemonSqueezy. getPaymentProcessorPlanId: () => string; effect: PaymentPlanEffect; diff --git a/template/app/src/server/validation.ts b/template/app/src/server/validation.ts new file mode 100644 index 0000000..43e2fc5 --- /dev/null +++ b/template/app/src/server/validation.ts @@ -0,0 +1,14 @@ +import { HttpError } from 'wasp/server'; +import * as z from 'zod'; + +export function ensureArgsSchemaOrThrowHttpError>( + schema: Schema, + rawArgs: unknown +): z.infer { + const parseResult = schema.safeParse(rawArgs); + if (!parseResult.success) { + console.error(parseResult.error); + throw new HttpError(400, 'Operation arguments validation failed', { errors: parseResult.error.errors }); + } + return parseResult.data; +} diff --git a/template/app/src/user/operations.ts b/template/app/src/user/operations.ts index 16765d9..e0303ba 100644 --- a/template/app/src/user/operations.ts +++ b/template/app/src/user/operations.ts @@ -1,15 +1,25 @@ -import { - type UpdateIsUserAdminById, - type GetPaginatedUsers, -} from 'wasp/server/operations'; +import * as z from 'zod'; +import { type UpdateIsUserAdminById, type GetPaginatedUsers } from 'wasp/server/operations'; import { type User } from 'wasp/entities'; import { HttpError } from 'wasp/server'; -import { type SubscriptionStatus } from '../payment/plans'; +import { subscriptionStatusSchema, type SubscriptionStatus } from '../payment/plans'; +import { ensureArgsSchemaOrThrowHttpError } from '../server/validation'; -export const updateIsUserAdminById: UpdateIsUserAdminById<{ id: string; data: Pick }, User> = async ( - { id, data }, +const updateUserAdminByIdInputSchema = z.object({ + id: z.string().nonempty(), + data: z.object({ + isAdmin: z.boolean(), + }), +}); + +type UpdateUserAdminByIdInput = z.infer; + +export const updateIsUserAdminById: UpdateIsUserAdminById = async ( + rawArgs: unknown, context ) => { + const args = ensureArgsSchemaOrThrowHttpError(updateUserAdminByIdInputSchema, rawArgs); + if (!context.user) { throw new HttpError(401); } @@ -20,39 +30,46 @@ export const updateIsUserAdminById: UpdateIsUserAdminById<{ id: string; data: Pi const updatedUser = await context.entities.User.update({ where: { - id, + id: args.id, }, data: { - isAdmin: data.isAdmin, + isAdmin: args.data.isAdmin, }, }); return updatedUser; }; -type GetPaginatedUsersInput = { - skip: number; - cursor?: number | undefined; - emailContains?: string; - isAdmin?: boolean; - subscriptionStatus?: SubscriptionStatus[]; -}; type GetPaginatedUsersOutput = { users: Pick[]; totalPages: number; }; +const getPaginatorArgsSchema = z.object({ + skip: z.number(), + cursor: z.number().optional(), + emailContains: z.string().nonempty().optional(), + isAdmin: z.boolean().optional(), + subscriptionStatus: z.array(subscriptionStatusSchema).optional(), +}); + +type GetPaginatedUsersInput = z.infer; + export const getPaginatedUsers: GetPaginatedUsers = async ( - args, + rawArgs: unknown, context ) => { + const args = ensureArgsSchemaOrThrowHttpError(getPaginatorArgsSchema, rawArgs); + if (!context.user?.isAdmin) { throw new HttpError(401); } - const allSubscriptionStatusOptions = args.subscriptionStatus as Array | undefined; - const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null) - let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as string[] | undefined + const allSubscriptionStatusOptions = args.subscriptionStatus; + const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null); + let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as + | string[] + | undefined; const queryResults = await context.entities.User.findMany({ skip: args.skip, From 077bede063843cf1f195b71d1bca56cb5d8f7f06 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Thu, 20 Feb 2025 22:38:54 +0100 Subject: [PATCH 2/9] Diffs. Cleanup. --- .../src/file-upload/operations.ts.diff | 12 +++++----- .../app_diff/src/payment/plans.ts.diff | 11 --------- .../app_diff/src/user/operations.ts.diff | 23 ++++--------------- template/app/src/file-upload/fileUploading.ts | 5 ++-- 4 files changed, 13 insertions(+), 38 deletions(-) delete mode 100644 opensaas-sh/app_diff/src/payment/plans.ts.diff diff --git a/opensaas-sh/app_diff/src/file-upload/operations.ts.diff b/opensaas-sh/app_diff/src/file-upload/operations.ts.diff index d7a58ae..00e6e3e 100644 --- a/opensaas-sh/app_diff/src/file-upload/operations.ts.diff +++ b/opensaas-sh/app_diff/src/file-upload/operations.ts.diff @@ -1,8 +1,8 @@ --- template/app/src/file-upload/operations.ts +++ opensaas-sh/app/src/file-upload/operations.ts -@@ -18,6 +18,18 @@ - throw new HttpError(401); - } +@@ -25,6 +25,18 @@ + + const args = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs); + const numberOfFilesByUser = await context.entities.File.count({ + where: { @@ -16,6 +16,6 @@ + throw new HttpError(403, 'Thanks for trying Open SaaS. This demo only allows 2 file uploads per user.'); + } + - const userInfo = context.user.id; - - const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo }); + const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ + fileType: args.fileType, + fileName: args.fileName, diff --git a/opensaas-sh/app_diff/src/payment/plans.ts.diff b/opensaas-sh/app_diff/src/payment/plans.ts.diff deleted file mode 100644 index a7cd30a..0000000 --- a/opensaas-sh/app_diff/src/payment/plans.ts.diff +++ /dev/null @@ -1,11 +0,0 @@ ---- template/app/src/payment/plans.ts -+++ opensaas-sh/app/src/payment/plans.ts -@@ -9,7 +9,7 @@ - } - - export interface PaymentPlan { -- // Returns the id under which this payment plan is identified on your payment processor. -+ // Returns the id under which this payment plan is identified on your payment processor. - // E.g. this might be price id on Stripe, or variant id on LemonSqueezy. - getPaymentProcessorPlanId: () => string; - effect: PaymentPlanEffect; diff --git a/opensaas-sh/app_diff/src/user/operations.ts.diff b/opensaas-sh/app_diff/src/user/operations.ts.diff index bbaefb0..8248910 100644 --- a/opensaas-sh/app_diff/src/user/operations.ts.diff +++ b/opensaas-sh/app_diff/src/user/operations.ts.diff @@ -1,8 +1,8 @@ --- template/app/src/user/operations.ts +++ opensaas-sh/app/src/user/operations.ts -@@ -38,7 +38,10 @@ - subscriptionStatus?: SubscriptionStatus[]; +@@ -41,7 +41,10 @@ }; + type GetPaginatedUsersOutput = { - users: Pick[]; + users: Pick< @@ -12,20 +12,7 @@ totalPages: number; }; -@@ -51,8 +54,10 @@ - } - - const allSubscriptionStatusOptions = args.subscriptionStatus as Array | undefined; -- const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null) -- let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as string[] | undefined -+ const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null); -+ let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as -+ | string[] -+ | undefined; - - const queryResults = await context.entities.User.findMany({ - skip: args.skip, -@@ -65,6 +70,7 @@ +@@ -82,6 +85,7 @@ mode: 'insensitive', }, isAdmin: args.isAdmin, @@ -33,7 +20,7 @@ }, { OR: [ -@@ -88,7 +94,7 @@ +@@ -105,7 +109,7 @@ username: true, isAdmin: true, subscriptionStatus: true, @@ -42,7 +29,7 @@ }, orderBy: { id: 'desc', -@@ -104,6 +110,7 @@ +@@ -121,6 +125,7 @@ mode: 'insensitive', }, isAdmin: args.isAdmin, diff --git a/template/app/src/file-upload/fileUploading.ts b/template/app/src/file-upload/fileUploading.ts index 9c2ebe2..43a6c5c 100644 --- a/template/app/src/file-upload/fileUploading.ts +++ b/template/app/src/file-upload/fileUploading.ts @@ -3,6 +3,8 @@ import { createFile } from 'wasp/client/operations'; import axios from 'axios'; import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from './validation'; +type AllowedFileType = (typeof ALLOWED_FILE_TYPES)[number]; +type FileWithValidType = Omit & { type: AllowedFileType }; interface FileUploadProgress { file: FileWithValidType; setUploadProgressPercent: Dispatch>; @@ -28,9 +30,6 @@ export interface FileUploadError { code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED'; } -type AllowedFileType = (typeof ALLOWED_FILE_TYPES)[number]; -type FileWithValidType = Omit & { type: AllowedFileType }; - type FileParseResult = | { kind: 'success'; file: FileWithValidType } | { From 3adfacc5edfcb02dfa4171c75f509ec07ec6675e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 21 Feb 2025 15:42:17 +0100 Subject: [PATCH 3/9] Fix abstraction leak in Switchers --- .../src/admin/dashboards/users/UsersTable.tsx | 4 +++- .../admin/elements/forms/FormElementsPage.tsx | 4 ++-- .../app/src/admin/elements/forms/SwitcherOne.tsx | 11 ++--------- .../app/src/admin/elements/forms/SwitcherTwo.tsx | 16 +++++++--------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/template/app/src/admin/dashboards/users/UsersTable.tsx b/template/app/src/admin/dashboards/users/UsersTable.tsx index f94b145..c16ee1c 100644 --- a/template/app/src/admin/dashboards/users/UsersTable.tsx +++ b/template/app/src/admin/dashboards/users/UsersTable.tsx @@ -8,7 +8,9 @@ import { updateIsUserAdminById } from 'wasp/client/operations'; import { type User } from 'wasp/entities'; const AdminSwitch = ({ id, isAdmin }: Pick) => { - return updateIsUserAdminById({ id: id, isAdmin: !isAdmin })} />; + return ( + updateIsUserAdminById({ id: id, isAdmin: value })} /> + ); }; const UsersTable = () => { diff --git a/template/app/src/admin/elements/forms/FormElementsPage.tsx b/template/app/src/admin/elements/forms/FormElementsPage.tsx index 3debb5d..7eacad2 100644 --- a/template/app/src/admin/elements/forms/FormElementsPage.tsx +++ b/template/app/src/admin/elements/forms/FormElementsPage.tsx @@ -309,8 +309,8 @@ function SwitchExamples() { const [isSecondOn, setIsSecondOn] = useState(false); return (
- setIsFirstOn(!isFirstOn)} /> - setIsSecondOn(!isSecondOn)} /> + +
); } diff --git a/template/app/src/admin/elements/forms/SwitcherOne.tsx b/template/app/src/admin/elements/forms/SwitcherOne.tsx index fabf701..7aef7e1 100644 --- a/template/app/src/admin/elements/forms/SwitcherOne.tsx +++ b/template/app/src/admin/elements/forms/SwitcherOne.tsx @@ -1,18 +1,11 @@ import { cn } from '../../../client/cn'; -import { ChangeEventHandler } from 'react'; -const SwitcherOne = ({ - isOn, - onChange, -}: { - isOn: boolean; - onChange: ChangeEventHandler; -}) => { +const SwitcherOne = ({ isOn, onChange }: { isOn: boolean; onChange: (value: boolean) => void }) => { return (