From f01d2414da000274de3bdced336c8883501516de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0o=C5=A1i=C4=87?= Date: Tue, 2 Jul 2024 14:36:25 +0200 Subject: [PATCH] Grouped all file-upload functionality. (#170) * Grouped all file-upload functionality. * fix --- .../operations.ts.diff} | 8 +-- .../app_diff/src/server/queries.ts.diff | 4 +- .../src/content/docs/guides/file-uploading.md | 25 ++----- .../src/content/docs/start/guided-tour.md | 6 +- template/app/main.wasp | 71 ++++++++++--------- .../app => file-upload}/FileUploadPage.tsx | 2 +- template/app/src/file-upload/operations.ts | 60 ++++++++++++++++ .../src/{server => }/file-upload/s3Utils.ts | 0 template/app/src/server/actions.ts | 29 +------- template/app/src/server/queries.ts | 28 +------- 10 files changed, 115 insertions(+), 118 deletions(-) rename opensaas-sh/app_diff/src/{server/actions.ts.diff => file-upload/operations.ts.diff} (73%) rename template/app/src/{client/app => file-upload}/FileUploadPage.tsx (99%) create mode 100644 template/app/src/file-upload/operations.ts rename template/app/src/{server => }/file-upload/s3Utils.ts (100%) diff --git a/opensaas-sh/app_diff/src/server/actions.ts.diff b/opensaas-sh/app_diff/src/file-upload/operations.ts.diff similarity index 73% rename from opensaas-sh/app_diff/src/server/actions.ts.diff rename to opensaas-sh/app_diff/src/file-upload/operations.ts.diff index 34077004..9a73cf6d 100644 --- a/opensaas-sh/app_diff/src/server/actions.ts.diff +++ b/opensaas-sh/app_diff/src/file-upload/operations.ts.diff @@ -1,6 +1,6 @@ ---- template/app/src/server/actions.ts -+++ opensaas-sh/app/src/server/actions.ts -@@ -318,6 +318,18 @@ +--- template/app/src/file-upload/operations.ts ++++ opensaas-sh/app/src/file-upload/operations.ts +@@ -21,6 +21,18 @@ throw new HttpError(401); } @@ -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.toString(); + const userInfo = context.user.id; const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo }); diff --git a/opensaas-sh/app_diff/src/server/queries.ts.diff b/opensaas-sh/app_diff/src/server/queries.ts.diff index f2021dcb..774af250 100644 --- a/opensaas-sh/app_diff/src/server/queries.ts.diff +++ b/opensaas-sh/app_diff/src/server/queries.ts.diff @@ -1,6 +1,6 @@ --- template/app/src/server/queries.ts +++ opensaas-sh/app/src/server/queries.ts -@@ -136,6 +136,7 @@ +@@ -110,6 +110,7 @@ mode: 'insensitive', }, isAdmin: args.isAdmin, @@ -8,7 +8,7 @@ }, { OR: [ -@@ -176,6 +177,7 @@ +@@ -150,6 +151,7 @@ mode: 'insensitive', }, isAdmin: args.isAdmin, diff --git a/opensaas-sh/blog/src/content/docs/guides/file-uploading.md b/opensaas-sh/blog/src/content/docs/guides/file-uploading.md index 8231506f..7a02649f 100644 --- a/opensaas-sh/blog/src/content/docs/guides/file-uploading.md +++ b/opensaas-sh/blog/src/content/docs/guides/file-uploading.md @@ -119,26 +119,13 @@ With your S3 bucket set up and your AWS credentials in place, you can now start To begin customizing file uploads, is important to know where everything lives in your app. Here's a quick overview: - `main.wasp`: - - The `File entity` can be found here. Here you can modify the fields to suit your needs: - ```c - entity File {=psl - id String @id @default(uuid()) - name String - type String - key String - uploadUrl String - user User @relation(fields: [userId], references: [id]) - userId Int - createdAt DateTime @default(now()) - psl=} - ``` -- `src/server/actions.ts`: + - The `File entity` can be found here. Here you can modify the fields to suit your needs. +- `src/file-upload/FileUploadPage.tsx`: + - The `FileUploadPage` component is where the file upload form lives. It also allows you to download the file from S3 by calling the `getDownloadFileSignedURL` based on that files `key` in the app DB. +- `src/file-upload/operations.ts`: - The `createFile` action lives here and calls the `getUploadFileSignedURLFromS3` within it using your AWS credentials before passing it to the client. This function stores the files in the S3 bucket within folders named after the user's ID, so that each user's files are stored separately. -- `src/server/queries.ts`: - - The `getAllFilesByUser` fetches all File information uploaded by the user. Note that the files do not exist in the app database, but rather the file data, name its `key`, which is used to fetch the file from S3 - - The `getDownloadFileSignedURL` query fetches the presigned URL for a file to be downloaded from S3 using the file's `key` stored in the app's database -- `src/client/app/FileUploadPage.tsx`: - - The `FileUploadPage` component is where the file upload form lives. It also allows you to download the file from S3 by calling the `getDownloadFileSignedURL` based on that files `key` in the app DB. + - The `getAllFilesByUser` fetches all File information uploaded by the user. Note that the files do not exist in the app database, but rather the file data, its name and its `key`, which is used to fetch the file from S3. + - The `getDownloadFileSignedURL` query fetches the presigned URL for a file to be downloaded from S3 using the file's `key` stored in the app's database. ## Using Multer to upload files to your server diff --git a/opensaas-sh/blog/src/content/docs/start/guided-tour.md b/opensaas-sh/blog/src/content/docs/start/guided-tour.md index 3b9cf81c..57eaae28 100644 --- a/opensaas-sh/blog/src/content/docs/start/guided-tour.md +++ b/opensaas-sh/blog/src/content/docs/start/guided-tour.md @@ -62,6 +62,7 @@ If you are using a version of the OpenSaaS template with Wasp `v0.11.x` or below │   ├── client/ # Your client code (React) goes here. │   ├── server/ # Your server code (NodeJS) goes here. │   ├── shared/ # Your shared (runtime independent) code goes here. +│   ├── file-upload/ # Logic for uploading files to S3. │   └── .waspignore ├── .env.server # Dev environment variables for your server code. ├── .env.client # Dev environment variables for your client code. @@ -101,7 +102,7 @@ It's possible to learn Wasp's feature set simply through using this template, bu ### Client -The `src/client` folder contains all the code that runs in the browser. It's a standard React app, with a few Wasp-specific things sprinkled in. +The `src/client` folder contains the code that runs in the browser. It's a standard React app, with a few Wasp-specific things sprinkled in. ```sh . @@ -120,14 +121,13 @@ The `src/client` folder contains all the code that runs in the browser. It's a s ### Server -The `src/server` folder contains all the code that runs on the server. Wasp compiles everything into a NodeJS server for you. +The `src/server` folder contains the code that runs on the server. Wasp compiles everything into a NodeJS server for you. All you have to do is define your server-side functions in the `main.wasp` file, write the logic in a function within `src/server` and Wasp will generate the boilerplate code for you. ```sh └── server    ├── auth # Some small auth-related functions to customize the auth flow. -   ├── file-upload # File upload utility functions.   ├── payments # Payments utility functions.   ├── scripts # Scripts to run via Wasp, e.g. database seeding.   ├── webhooks # The webhook handler for Stripe. diff --git a/template/app/main.wasp b/template/app/main.wasp index 2af5bd06..7a74f5cf 100644 --- a/template/app/main.wasp +++ b/template/app/main.wasp @@ -135,19 +135,6 @@ entity Task {=psl isDone Boolean @default(false) psl=} -entity File {=psl - id String @id @default(uuid()) - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - userId String - - name String - type String - key String - uploadUrl String -psl=} - // TODO: add functionality to allow users to send messages to admin // and make them accessible via the admin dashboard entity ContactFormMessage {=psl @@ -257,12 +244,6 @@ page CheckoutPage { component: import Checkout from "@src/client/app/CheckoutPage" } -route FileUploadRoute { path: "/file-upload", to: FileUploadPage } -page FileUploadPage { - authRequired: true, - component: import FileUpload from "@src/client/app/FileUploadPage" -} - //#region Admin Pages route AdminRoute { path: "/admin", to: DashboardPage } page DashboardPage { @@ -367,11 +348,6 @@ action updateUserById { entities: [User] } -action createFile { - fn: import { createFile } from "@src/server/actions.js", - entities: [User, File] -} - // 📚 Queries @@ -385,16 +361,6 @@ query getAllTasksByUser { entities: [Task] } -query getAllFilesByUser { - fn: import { getAllFilesByUser } from "@src/server/queries.js", - entities: [User, File] -} - -query getDownloadFileSignedURL { - fn: import { getDownloadFileSignedURL } from "@src/server/queries.js", - entities: [User, File] -} - query getDailyStats { fn: import { getDailyStats } from "@src/server/queries.js", entities: [User, DailyStats] @@ -444,3 +410,40 @@ job dailyStatsJob { }, entities: [User, DailyStats, Logs, PageViewSource] } + + +//#region File Upload +route FileUploadRoute { path: "/file-upload", to: FileUploadPage } +page FileUploadPage { + authRequired: true, + component: import FileUpload from "@src/file-upload/FileUploadPage" +} + +action createFile { + fn: import { createFile } from "@src/file-upload/operations", + entities: [User, File] +} + +query getAllFilesByUser { + fn: import { getAllFilesByUser } from "@src/file-upload/operations", + entities: [User, File] +} + +query getDownloadFileSignedURL { + fn: import { getDownloadFileSignedURL } from "@src/file-upload/operations", + entities: [User, File] +} + +entity File {=psl + id String @id @default(uuid()) + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + userId String + + name String + type String + key String + uploadUrl String +psl=} +//#endregion \ No newline at end of file diff --git a/template/app/src/client/app/FileUploadPage.tsx b/template/app/src/file-upload/FileUploadPage.tsx similarity index 99% rename from template/app/src/client/app/FileUploadPage.tsx rename to template/app/src/file-upload/FileUploadPage.tsx index c5aa4470..862a4457 100644 --- a/template/app/src/client/app/FileUploadPage.tsx +++ b/template/app/src/file-upload/FileUploadPage.tsx @@ -1,7 +1,7 @@ import { createFile, useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations'; import axios from 'axios'; import { useState, useEffect, FormEvent } from 'react'; -import { cn } from '../../shared/utils'; +import { cn } from '../shared/utils'; export default function FileUploadPage() { const [fileToDownload, setFileToDownload] = useState(''); diff --git a/template/app/src/file-upload/operations.ts b/template/app/src/file-upload/operations.ts new file mode 100644 index 00000000..fbc5df5f --- /dev/null +++ b/template/app/src/file-upload/operations.ts @@ -0,0 +1,60 @@ +import { HttpError } from 'wasp/server'; +import { type File } from 'wasp/entities'; +import { + type CreateFile, + type GetAllFilesByUser, + type GetDownloadFileSignedURL, +} from 'wasp/server/operations'; + +import { + getUploadFileSignedURLFromS3, + getDownloadFileSignedURLFromS3 +} from './s3Utils'; + +type FileDescription = { + fileType: string; + name: string; +}; + +export const createFile: CreateFile = async ({ fileType, name }, context) => { + if (!context.user) { + throw new HttpError(401); + } + + const userInfo = context.user.id; + + const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo }); + + return await context.entities.File.create({ + data: { + name, + key, + uploadUrl, + type: fileType, + user: { connect: { id: context.user.id } }, + }, + }); +}; + +export const getAllFilesByUser: GetAllFilesByUser = async (_args, context) => { + if (!context.user) { + throw new HttpError(401); + } + return context.entities.File.findMany({ + where: { + user: { + id: context.user.id, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); +}; + +export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async ( + { key }, + _context +) => { + return await getDownloadFileSignedURLFromS3({ key }); +}; diff --git a/template/app/src/server/file-upload/s3Utils.ts b/template/app/src/file-upload/s3Utils.ts similarity index 100% rename from template/app/src/server/file-upload/s3Utils.ts rename to template/app/src/file-upload/s3Utils.ts diff --git a/template/app/src/server/actions.ts b/template/app/src/server/actions.ts index e485a2e5..5ca4cea3 100644 --- a/template/app/src/server/actions.ts +++ b/template/app/src/server/actions.ts @@ -1,4 +1,4 @@ -import { type User, type Task, type File } from 'wasp/entities'; +import { type User, type Task } from 'wasp/entities'; import { HttpError } from 'wasp/server'; import { type GenerateGptResponse, @@ -8,13 +8,11 @@ import { type CreateTask, type DeleteTask, type UpdateTask, - type CreateFile, } from 'wasp/server/operations'; import Stripe from 'stripe'; import type { GeneratedSchedule, StripePaymentResult } from '../shared/types'; import { fetchStripeCustomer, createStripeCheckoutSession } from './payments/stripeUtils.js'; import { TierIds } from '../shared/constants.js'; -import { getUploadFileSignedURLFromS3 } from './file-upload/s3Utils.js'; import OpenAI from 'openai'; const openai = setupOpenAI(); @@ -308,31 +306,6 @@ export const updateUserById: UpdateUserById<{ id: string; data: Partial }, return updatedUser; }; -type FileDescription = { - fileType: string; - name: string; -}; - -export const createFile: CreateFile = async ({ fileType, name }, context) => { - if (!context.user) { - throw new HttpError(401); - } - - const userInfo = context.user.id; - - const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo }); - - return await context.entities.File.create({ - data: { - name, - key, - uploadUrl, - type: fileType, - user: { connect: { id: context.user.id } }, - }, - }); -}; - export const updateCurrentUser: UpdateCurrentUser, User> = async (user, context) => { if (!context.user) { throw new HttpError(401); diff --git a/template/app/src/server/queries.ts b/template/app/src/server/queries.ts index 3913fc44..c1895486 100644 --- a/template/app/src/server/queries.ts +++ b/template/app/src/server/queries.ts @@ -1,14 +1,11 @@ -import { type DailyStats, type GptResponse, type User, type PageViewSource, type Task, type File } from 'wasp/entities'; +import { type DailyStats, type GptResponse, type User, type PageViewSource, type Task } from 'wasp/entities'; import { HttpError } from 'wasp/server'; import { type GetGptResponses, type GetDailyStats, type GetPaginatedUsers, type GetAllTasksByUser, - type GetAllFilesByUser, - type GetDownloadFileSignedURL, } from 'wasp/server/operations'; -import { getDownloadFileSignedURLFromS3 } from './file-upload/s3Utils.js'; import { type SubscriptionStatusOptions } from '../shared/types.js'; type DailyStatsWithSources = DailyStats & { @@ -49,29 +46,6 @@ export const getAllTasksByUser: GetAllTasksByUser = async (_args, }); }; -export const getAllFilesByUser: GetAllFilesByUser = async (_args, context) => { - if (!context.user) { - throw new HttpError(401); - } - return context.entities.File.findMany({ - where: { - user: { - id: context.user.id, - }, - }, - orderBy: { - createdAt: 'desc', - }, - }); -}; - -export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async ( - { key }, - _context -) => { - return await getDownloadFileSignedURLFromS3({ key }); -}; - export const getDailyStats: GetDailyStats = async (_args, context) => { if (!context.user?.isAdmin) { throw new HttpError(401);