Skip to content

Commit

Permalink
File Backup on Quadratic Cloud (#425)
Browse files Browse the repository at this point in the history
* File Backup on Quadratic API

* add prisma command to Procfile

* when backing up file record version and created / modified dates

* add comment

* send JSON response

* lower file size limit

* increase requests allowed to 30 per minute
  • Loading branch information
davidkircos committed Apr 14, 2023
1 parent 8e29669 commit 9bb2772
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 37 deletions.
4 changes: 3 additions & 1 deletion quadratic-api/Procfile
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
web: node dist/index.js
web: node dist/index.js

release: npx prisma migrate deploy
66 changes: 33 additions & 33 deletions quadratic-api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions quadratic-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"author": "David Kircos",
"license": "MIT",
"dependencies": {
"@prisma/client": "^4.6.1",
"@prisma/client": "^4.12.0",
"@types/express": "^4.17.14",
"@types/node": "^18.11.9",
"cors": "^2.8.5",
Expand All @@ -33,7 +33,7 @@
"devDependencies": {
"@types/cors": "^2.8.12",
"dotenv-cli": "^7.1.0",
"prisma": "^4.6.1"
"prisma": "^4.12.0"
},
"engines": {
"node": "18.x"
Expand Down
29 changes: 29 additions & 0 deletions quadratic-api/prisma/migrations/20230413184442_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "QUser" (
"id" SERIAL NOT NULL,
"auth0_user_id" TEXT,

CONSTRAINT "QUser_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "QFile" (
"id" SERIAL NOT NULL,
"uuid" TEXT NOT NULL,
"name" TEXT NOT NULL,
"contents" JSONB NOT NULL,
"created_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"qUserId" INTEGER NOT NULL,

CONSTRAINT "QFile_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "QUser_auth0_user_id_key" ON "QUser"("auth0_user_id");

-- CreateIndex
CREATE UNIQUE INDEX "QFile_uuid_key" ON "QFile"("uuid");

-- AddForeignKey
ALTER TABLE "QFile" ADD CONSTRAINT "QFile_qUserId_fkey" FOREIGN KEY ("qUserId") REFERENCES "QUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "QFile" ADD COLUMN "times_updated" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "version" TEXT;
3 changes: 3 additions & 0 deletions quadratic-api/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
32 changes: 32 additions & 0 deletions quadratic-api/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

// Models
model QUser {
id Int @id @default(autoincrement())
auth0_user_id String? @unique
QFile QFile[]
}

model QFile {
id Int @id @default(autoincrement())
uuid String @unique @default(uuid())
user_owner QUser @relation(fields: [qUserId], references: [id])
name String
contents Json
created_date DateTime @default(now())
updated_date DateTime @default(now())
qUserId Int
// analytics fields
times_updated Int @default(1)
version String?
}
13 changes: 13 additions & 0 deletions quadratic-api/src/helpers/get_file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PrismaClient, QUser } from '@prisma/client';

const prisma = new PrismaClient();

export const get_file = async (user: QUser, uuid: string) => {
// Get the file from the database, only if it exists and the user owns it
return await prisma.qFile.findFirst({
where: {
qUserId: user.id, // important to prevent users from getting access to files they don't own
uuid,
},
});
};
16 changes: 16 additions & 0 deletions quadratic-api/src/helpers/get_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Request as JWTRequest } from 'express-jwt';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const get_user = async (request: JWTRequest) => {
return await prisma.qUser.upsert({
where: {
auth0_user_id: request.auth?.sub,
},
update: {},
create: {
auth0_user_id: request.auth?.sub,
},
});
};
18 changes: 18 additions & 0 deletions quadratic-api/src/helpers/read_file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const get_file_metadata = (file_contents: any) => {
try {
return {
name: file_contents.filename,
version: file_contents.version,
modified: file_contents.modified,
created: file_contents.created,
};
} catch (e) {
console.error(e);
return {
name: undefined,
version: undefined,
modified: undefined,
created: undefined,
};
}
};
4 changes: 3 additions & 1 deletion quadratic-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import ai_chat_router from './routes/ai_chat';
import helmet from 'helmet';
import files_router from './routes/files';

const app = express();
app.use(express.json());
app.use(express.json({ limit: '5mb' }));
app.use(helmet());

// set CORS origin from env variable
Expand All @@ -21,6 +22,7 @@ app.use((req, res, next) => {

// Routes
app.use('/ai', ai_chat_router);
app.use('/v0/files', files_router);

// Error-logging middleware
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
Expand Down
72 changes: 72 additions & 0 deletions quadratic-api/src/routes/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import express from 'express';
import { validateAccessToken } from '../middleware/auth';
import { Request as JWTRequest } from 'express-jwt';
import { z } from 'zod';
import rateLimit from 'express-rate-limit';
import { PrismaClient } from '@prisma/client';
import { get_user } from '../helpers/get_user';
import { get_file } from '../helpers/get_file';
import { get_file_metadata } from '../helpers/read_file';

const files_router = express.Router();
const prisma = new PrismaClient();

const ai_rate_limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 30, // Limit number of requests per windowMs
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
keyGenerator: (request: JWTRequest, response) => {
return request.auth?.sub || 'anonymous';
},
});

const FilesBackupRequestBody = z.object({
uuid: z.string(),
fileContents: z.any(),
});

// type FilesBackupRequestBodyType = z.infer<typeof FilesBackupRequestBody>;

files_router.post('/backup', validateAccessToken, ai_rate_limiter, async (request: JWTRequest, response) => {
const r_json = FilesBackupRequestBody.parse(request.body);

const user = await get_user(request);
const file = await get_file(user, r_json.uuid);

const file_contents = JSON.parse(r_json.fileContents);
const file_metadata = get_file_metadata(file_contents);

if (file) {
await prisma.qFile.update({
where: {
id: file.id,
},
data: {
name: file_metadata.name,
contents: file_contents,
updated_date: new Date(file_metadata.modified),
version: file_metadata.version,
times_updated: {
increment: 1,
},
},
});
} else {
await prisma.qFile.create({
data: {
qUserId: user.id,
uuid: r_json.uuid,
name: file_metadata.name,
contents: file_contents,
created_date: new Date(file_metadata.created),
updated_date: new Date(file_metadata.modified),
version: file_metadata.version,
},
});
}

response.status(200).json({ message: 'File backup successful.' });
});

export default files_router;
Loading

0 comments on commit 9bb2772

Please sign in to comment.