Grouped all file-upload functionality. (#170)

* Grouped all file-upload functionality.

* fix
This commit is contained in:
Martin Šošić
2024-07-02 14:36:25 +02:00
committed by GitHub
parent 25ffc25a0b
commit f01d2414da
10 changed files with 115 additions and 118 deletions

View File

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

View File

@@ -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,

View File

@@ -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 `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 `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.
- 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

View File

@@ -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.

View File

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

View File

@@ -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<string>('');

View File

@@ -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<FileDescription, File> = 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<void, File[]> = 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 });
};

View File

@@ -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<User> },
return updatedUser;
};
type FileDescription = {
fileType: string;
name: string;
};
export const createFile: CreateFile<FileDescription, File> = 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<Partial<User>, User> = async (user, context) => {
if (!context.user) {
throw new HttpError(401);

View File

@@ -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<void, Task[]> = async (_args,
});
};
export const getAllFilesByUser: GetAllFilesByUser<void, File[]> = 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<void, DailyStatsValues> = async (_args, context) => {
if (!context.user?.isAdmin) {
throw new HttpError(401);