Skip to content

Commit 859c4ac

Browse files
committed
Merge branch 'main' into filip-refactor-user-module
2 parents 88da62d + 53c4077 commit 859c4ac

File tree

13 files changed

+220
-128
lines changed

13 files changed

+220
-128
lines changed
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
--- template/app/src/file-upload/operations.ts
22
+++ opensaas-sh/app/src/file-upload/operations.ts
3-
@@ -18,6 +18,18 @@
4-
throw new HttpError(401);
5-
}
3+
@@ -25,6 +25,18 @@
4+
5+
const { fileType, fileName } = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs);
66

77
+ const numberOfFilesByUser = await context.entities.File.count({
88
+ where: {
@@ -16,6 +16,6 @@
1616
+ throw new HttpError(403, 'Thanks for trying Open SaaS. This demo only allows 2 file uploads per user.');
1717
+ }
1818
+
19-
const userInfo = context.user.id;
20-
21-
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo });
19+
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({
20+
fileType,
21+
fileName,

opensaas-sh/app_diff/src/payment/plans.ts.diff

Lines changed: 0 additions & 11 deletions
This file was deleted.

opensaas-sh/app_diff/src/user/operations.ts.diff

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
--- template/app/src/user/operations.ts
22
+++ opensaas-sh/app/src/user/operations.ts
3-
@@ -38,7 +38,10 @@
4-
subscriptionStatus?: SubscriptionStatus[];
3+
@@ -41,7 +41,10 @@
54
};
5+
66
type GetPaginatedUsersOutput = {
77
- users: Pick<User, 'id' | 'email' | 'username' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
88
+ users: Pick<
@@ -12,28 +12,15 @@
1212
totalPages: number;
1313
};
1414

15-
@@ -51,8 +54,10 @@
16-
}
17-
18-
const allSubscriptionStatusOptions = args.subscriptionStatus as Array<string | null> | undefined;
19-
- const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null)
20-
- let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as string[] | undefined
21-
+ const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null);
22-
+ let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as
23-
+ | string[]
24-
+ | undefined;
25-
26-
const queryResults = await context.entities.User.findMany({
27-
skip: args.skip,
28-
@@ -65,6 +70,7 @@
15+
@@ -85,6 +88,7 @@
2916
mode: 'insensitive',
3017
},
31-
isAdmin: args.isAdmin,
18+
isAdmin,
3219
+ isMockUser: true,
3320
},
3421
{
3522
OR: [
36-
@@ -88,7 +94,7 @@
23+
@@ -108,7 +112,7 @@
3724
username: true,
3825
isAdmin: true,
3926
subscriptionStatus: true,
@@ -42,10 +29,10 @@
4229
},
4330
orderBy: {
4431
id: 'desc',
45-
@@ -104,6 +110,7 @@
32+
@@ -124,6 +128,7 @@
4633
mode: 'insensitive',
4734
},
48-
isAdmin: args.isAdmin,
35+
isAdmin,
4936
+ isMockUser: true,
5037
},
5138
{

template/app/src/demo-ai-app/operations.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as z from 'zod';
12
import type { Task, GptResponse } from 'wasp/entities';
23
import type {
34
GenerateGptResponse,
@@ -10,6 +11,7 @@ import type {
1011
import { HttpError } from 'wasp/server';
1112
import { GeneratedSchedule } from './schedule';
1213
import OpenAI from 'openai';
14+
import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
1315

1416
const openai = setupOpenAI();
1517
function setupOpenAI() {
@@ -20,15 +22,23 @@ function setupOpenAI() {
2022
}
2123

2224
//#region Actions
23-
type GptPayload = {
24-
hours: string;
25-
};
2625

27-
export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSchedule> = async ({ hours }, context) => {
26+
const generateGptResponseInputSchema = z.object({
27+
hours: z.string().regex(/^\d+(\.\d+)?$/, 'Hours must be a number'),
28+
});
29+
30+
type GenerateGptResponseInput = z.infer<typeof generateGptResponseInputSchema>;
31+
32+
export const generateGptResponse: GenerateGptResponse<GenerateGptResponseInput, GeneratedSchedule> = async (
33+
rawArgs,
34+
context
35+
) => {
2836
if (!context.user) {
2937
throw new HttpError(401);
3038
}
3139

40+
const { hours } = ensureArgsSchemaOrThrowHttpError(generateGptResponseInputSchema, rawArgs);
41+
3242
const tasks = await context.entities.Task.findMany({
3343
where: {
3444
user: {
@@ -181,11 +191,19 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSched
181191
}
182192
};
183193

184-
export const createTask: CreateTask<Pick<Task, 'description'>, Task> = async ({ description }, context) => {
194+
const createTaskInputSchema = z.object({
195+
description: z.string().nonempty(),
196+
});
197+
198+
type CreateTaskInput = z.infer<typeof createTaskInputSchema>;
199+
200+
export const createTask: CreateTask<CreateTaskInput, Task> = async (rawArgs, context) => {
185201
if (!context.user) {
186202
throw new HttpError(401);
187203
}
188204

205+
const { description } = ensureArgsSchemaOrThrowHttpError(createTaskInputSchema, rawArgs);
206+
189207
const task = await context.entities.Task.create({
190208
data: {
191209
description,
@@ -196,11 +214,21 @@ export const createTask: CreateTask<Pick<Task, 'description'>, Task> = async ({
196214
return task;
197215
};
198216

199-
export const updateTask: UpdateTask<Partial<Task>, Task> = async ({ id, isDone, time }, context) => {
217+
const updateTaskInputSchema = z.object({
218+
id: z.string().nonempty(),
219+
isDone: z.boolean().optional(),
220+
time: z.string().optional(),
221+
});
222+
223+
type UpdateTaskInput = z.infer<typeof updateTaskInputSchema>;
224+
225+
export const updateTask: UpdateTask<UpdateTaskInput, Task> = async (rawArgs, context) => {
200226
if (!context.user) {
201227
throw new HttpError(401);
202228
}
203229

230+
const { id, isDone, time } = ensureArgsSchemaOrThrowHttpError(updateTaskInputSchema, rawArgs);
231+
204232
const task = await context.entities.Task.update({
205233
where: {
206234
id,
@@ -214,11 +242,19 @@ export const updateTask: UpdateTask<Partial<Task>, Task> = async ({ id, isDone,
214242
return task;
215243
};
216244

217-
export const deleteTask: DeleteTask<Pick<Task, 'id'>, Task> = async ({ id }, context) => {
245+
const deleteTaskInputSchema = z.object({
246+
id: z.string().nonempty(),
247+
});
248+
249+
type DeleteTaskInput = z.infer<typeof deleteTaskInputSchema>;
250+
251+
export const deleteTask: DeleteTask<DeleteTaskInput, Task> = async (rawArgs, context) => {
218252
if (!context.user) {
219253
throw new HttpError(401);
220254
}
221255

256+
const { id } = ensureArgsSchemaOrThrowHttpError(deleteTaskInputSchema, rawArgs);
257+
222258
const task = await context.entities.Task.delete({
223259
where: {
224260
id,

template/app/src/file-upload/FileUploadPage.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ import { cn } from '../client/cn';
22
import { useState, useEffect, FormEvent } from 'react';
33
import type { File } from 'wasp/entities';
44
import { useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations';
5-
import { type FileUploadError, uploadFileWithProgress, validateFile, ALLOWED_FILE_TYPES } from './fileUploading';
5+
import {
6+
type FileWithValidType,
7+
type FileUploadError,
8+
validateFile,
9+
uploadFileWithProgress,
10+
} from './fileUploading';
11+
import { ALLOWED_FILE_TYPES } from './validation';
612

713
export default function FileUploadPage() {
814
const [fileKeyForS3, setFileKeyForS3] = useState<File['key']>('');
915
const [uploadProgressPercent, setUploadProgressPercent] = useState<number>(0);
1016
const [uploadError, setUploadError] = useState<FileUploadError | null>(null);
1117

1218
const allUserFiles = useQuery(getAllFilesByUser, undefined, {
13-
// We disable automatic refetching because otherwise files would be refetched after `createFile` is called and the S3 URL is returned,
19+
// We disable automatic refetching because otherwise files would be refetched after `createFile` is called and the S3 URL is returned,
1420
// which happens before the file is actually fully uploaded. Instead, we manually (re)fetch on mount and after the upload is complete.
1521
enabled: false,
1622
});
@@ -64,13 +70,13 @@ export default function FileUploadPage() {
6470
return;
6571
}
6672

67-
const validationError = validateFile(file);
68-
if (validationError) {
69-
setUploadError(validationError);
73+
const fileValidationError = validateFile(file);
74+
if (fileValidationError !== null) {
75+
setUploadError(fileValidationError);
7076
return;
7177
}
7278

73-
await uploadFileWithProgress({ file, setUploadProgressPercent });
79+
await uploadFileWithProgress({ file: file as FileWithValidType, setUploadProgressPercent });
7480
formElement.reset();
7581
allUserFiles.refetch();
7682
} catch (error) {
@@ -117,11 +123,11 @@ export default function FileUploadPage() {
117123
<>
118124
<span>Uploading {uploadProgressPercent}%</span>
119125
<div
120-
role="progressbar"
126+
role='progressbar'
121127
aria-valuenow={uploadProgressPercent}
122128
aria-valuemin={0}
123129
aria-valuemax={100}
124-
className="absolute bottom-0 left-0 h-1 bg-yellow-500 transition-all duration-300 ease-in-out rounded-b-md"
130+
className='absolute bottom-0 left-0 h-1 bg-yellow-500 transition-all duration-300 ease-in-out rounded-b-md'
125131
style={{ width: `${uploadProgressPercent}%` }}
126132
></div>
127133
</>
Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
11
import { Dispatch, SetStateAction } from 'react';
22
import { createFile } from 'wasp/client/operations';
33
import axios from 'axios';
4+
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from './validation';
45

6+
export type FileWithValidType = Omit<File, 'type'> & { type: AllowedFileType };
7+
type AllowedFileType = (typeof ALLOWED_FILE_TYPES)[number];
58
interface FileUploadProgress {
6-
file: File;
9+
file: FileWithValidType;
710
setUploadProgressPercent: Dispatch<SetStateAction<number>>;
811
}
912

10-
export interface FileUploadError {
11-
message: string;
12-
code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED';
13-
}
14-
15-
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // Set this to the max file size you want to allow (currently 5MB).
16-
export const ALLOWED_FILE_TYPES = [
17-
'image/jpeg',
18-
'image/png',
19-
'application/pdf',
20-
'text/*',
21-
'video/quicktime',
22-
'video/mp4',
23-
];
24-
2513
export async function uploadFileWithProgress({ file, setUploadProgressPercent }: FileUploadProgress) {
26-
const { uploadUrl } = await createFile({ fileType: file.type, name: file.name });
27-
return await axios.put(uploadUrl, file, {
14+
const { uploadUrl } = await createFile({ fileType: file.type, fileName: file.name });
15+
return axios.put(uploadUrl, file, {
2816
headers: {
2917
'Content-Type': file.type,
3018
},
@@ -37,18 +25,29 @@ export async function uploadFileWithProgress({ file, setUploadProgressPercent }:
3725
});
3826
}
3927

40-
export function validateFile(file: File): FileUploadError | null {
28+
export interface FileUploadError {
29+
message: string;
30+
code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED';
31+
}
32+
33+
export function validateFile(file: File) {
4134
if (file.size > MAX_FILE_SIZE) {
4235
return {
4336
message: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
44-
code: 'FILE_TOO_LARGE',
37+
code: 'FILE_TOO_LARGE' as const,
4538
};
4639
}
47-
if (!ALLOWED_FILE_TYPES.includes(file.type)) {
40+
41+
if (!isAllowedFileType(file.type)) {
4842
return {
4943
message: `File type '${file.type}' is not supported.`,
50-
code: 'INVALID_FILE_TYPE',
44+
code: 'INVALID_FILE_TYPE' as const,
5145
};
5246
}
47+
5348
return null;
5449
}
50+
51+
function isAllowedFileType(fileType: string): fileType is AllowedFileType {
52+
return (ALLOWED_FILE_TYPES as readonly string[]).includes(fileType);
53+
}

template/app/src/file-upload/operations.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as z from 'zod';
12
import { HttpError } from 'wasp/server';
23
import { type File } from 'wasp/entities';
34
import {
@@ -7,24 +8,32 @@ import {
78
} from 'wasp/server/operations';
89

910
import { getUploadFileSignedURLFromS3, getDownloadFileSignedURLFromS3 } from './s3Utils';
11+
import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
12+
import { ALLOWED_FILE_TYPES } from './validation';
1013

11-
type FileDescription = {
12-
fileType: string;
13-
name: string;
14-
};
14+
const createFileInputSchema = z.object({
15+
fileType: z.enum(ALLOWED_FILE_TYPES),
16+
fileName: z.string().nonempty(),
17+
});
18+
19+
type CreateFileInput = z.infer<typeof createFileInputSchema>;
1520

16-
export const createFile: CreateFile<FileDescription, File> = async ({ fileType, name }, context) => {
21+
export const createFile: CreateFile<CreateFileInput, File> = async (rawArgs, context) => {
1722
if (!context.user) {
1823
throw new HttpError(401);
1924
}
2025

21-
const userInfo = context.user.id;
26+
const { fileType, fileName } = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs);
2227

23-
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo });
28+
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({
29+
fileType,
30+
fileName,
31+
userId: context.user.id,
32+
});
2433

2534
return await context.entities.File.create({
2635
data: {
27-
name,
36+
name: fileName,
2837
key,
2938
uploadUrl,
3039
type: fileType,
@@ -49,9 +58,14 @@ export const getAllFilesByUser: GetAllFilesByUser<void, File[]> = async (_args,
4958
});
5059
};
5160

52-
export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async (
53-
{ key },
54-
_context
55-
) => {
61+
const getDownloadFileSignedURLInputSchema = z.object({ key: z.string().nonempty() });
62+
63+
type GetDownloadFileSignedURLInput = z.infer<typeof getDownloadFileSignedURLInputSchema>;
64+
65+
export const getDownloadFileSignedURL: GetDownloadFileSignedURL<
66+
GetDownloadFileSignedURLInput,
67+
string
68+
> = async (rawArgs, _context) => {
69+
const { key } = ensureArgsSchemaOrThrowHttpError(getDownloadFileSignedURLInputSchema, rawArgs);
5670
return await getDownloadFileSignedURLFromS3({ key });
5771
};

0 commit comments

Comments
 (0)