mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-11-25 16:10:20 +01:00
Grouped all file-upload functionality. (#170)
* Grouped all file-upload functionality. * fix
This commit is contained in:
@@ -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 });
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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>('');
|
||||
60
template/app/src/file-upload/operations.ts
Normal file
60
template/app/src/file-upload/operations.ts
Normal 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 });
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user