mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-13 16:04:36 -04:00
feat(api): add prisma as orm (#49413)
This commit is contained in:
committed by
GitHub
parent
ca2086cacb
commit
fa7955dc75
18
api/README.md
Normal file
18
api/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Connecting to local database
|
||||
|
||||
The api uses the ORM Prisma and it needs the MongoDB instance to be a replica set.
|
||||
|
||||
## Atlas
|
||||
|
||||
If you use MongoDB Atlas, the set is managed for you.
|
||||
|
||||
## Local
|
||||
|
||||
The simplest way to run a replica set locally is to use the docker-compose file
|
||||
in /tools. First disable any running MongoDB instance on your machin, then run
|
||||
the docker-compose file.
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import fastifyMongo from '@fastify/mongodb';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
import { MONGOHQ_URL } from '../utils/env';
|
||||
|
||||
async function connect(fastify: FastifyInstance) {
|
||||
fastify.log.info(`Connecting to Mongodb`);
|
||||
await fastify.register(fastifyMongo, {
|
||||
url: MONGOHQ_URL
|
||||
});
|
||||
}
|
||||
|
||||
export const dbConnector = fastifyPlugin(connect);
|
||||
23
api/db/prisma.ts
Normal file
23
api/db/prisma.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
prisma: PrismaClient;
|
||||
}
|
||||
}
|
||||
|
||||
const prismaPlugin: FastifyPluginAsync = fp(async (server, _options) => {
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
await prisma.$connect();
|
||||
|
||||
server.decorate('prisma', prisma);
|
||||
|
||||
server.addHook('onClose', async server => {
|
||||
await server.prisma.$disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
export default prismaPlugin;
|
||||
@@ -9,8 +9,8 @@ import jwtAuthz from './plugins/fastify-jwt-authz';
|
||||
import sessionAuth from './plugins/session-auth';
|
||||
import { testRoutes } from './routes/test';
|
||||
import { auth0Routes } from './routes/auth0';
|
||||
import { dbConnector } from './db';
|
||||
import { testMiddleware } from './middleware';
|
||||
|
||||
import {
|
||||
AUTH0_AUDIENCE,
|
||||
AUTH0_DOMAIN,
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
SESSION_SECRET
|
||||
} from './utils/env';
|
||||
|
||||
import prismaPlugin from './db/prisma';
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
|
||||
});
|
||||
@@ -55,7 +57,8 @@ const start = async () => {
|
||||
|
||||
void fastify.use('/test', testMiddleware);
|
||||
|
||||
void fastify.register(dbConnector);
|
||||
void fastify.register(prismaPlugin);
|
||||
|
||||
void fastify.register(testRoutes);
|
||||
void fastify.register(auth0Routes, { prefix: '/auth0' });
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
"@fastify/middie": "8.1",
|
||||
"@fastify/mongodb": "6.2.0",
|
||||
"@fastify/session": "^10.1.1",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"@prisma/client": "4.10.1",
|
||||
"connect-mongo": "4.6.0",
|
||||
"fastify": "4.14.1",
|
||||
"fastify-auth0-verify": "^1.0.0",
|
||||
"fastify-plugin": "^4.3.0",
|
||||
@@ -40,7 +40,12 @@
|
||||
"build": "tsc",
|
||||
"develop": "nodemon index.ts",
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"test": "node --test -r ts-node/register **/*.test.ts"
|
||||
"test": "node --test -r ts-node/register **/*.test.ts",
|
||||
"prisma": "MONGOHQ_URL=mongodb://localhost:27017/freecodecamp?directConnection=true prisma",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"version": "0.0.1"
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"prisma": "4.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
144
api/prisma/schema.prisma
Normal file
144
api/prisma/schema.prisma
Normal file
@@ -0,0 +1,144 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mongodb"
|
||||
url = env("MONGOHQ_URL")
|
||||
}
|
||||
|
||||
type DonationStartDate {
|
||||
date DateTime @map("_date") @db.Date
|
||||
when String @map("_when")
|
||||
}
|
||||
|
||||
type UserBadges {
|
||||
/// Could not determine type: the field only had null or empty values in the sample set.
|
||||
coreTeam Json?
|
||||
}
|
||||
|
||||
type UserCompletedChallenges {
|
||||
completedDate Float
|
||||
id String
|
||||
}
|
||||
|
||||
type UserProfileUi {
|
||||
isLocked Boolean
|
||||
showAbout Boolean
|
||||
showCerts Boolean
|
||||
showDonation Boolean
|
||||
showHeatMap Boolean
|
||||
showLocation Boolean
|
||||
showName Boolean
|
||||
showPoints Boolean
|
||||
showPortfolio Boolean
|
||||
showTimeLine Boolean
|
||||
}
|
||||
|
||||
model AccessToken {
|
||||
id String @id @map("_id")
|
||||
created DateTime @db.Date
|
||||
/// Multiple data types found: Float: 70.3%, Int: 29.7% out of 118 sampled entries
|
||||
ttl Json
|
||||
userId String @db.ObjectId
|
||||
}
|
||||
|
||||
model AuthToken {
|
||||
id String @id @map("_id")
|
||||
created DateTime @db.Date
|
||||
ttl Int
|
||||
userId String @db.ObjectId
|
||||
}
|
||||
|
||||
model Donation {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
amount Int
|
||||
customerId String
|
||||
duration String
|
||||
email String
|
||||
provider String
|
||||
startDate DonationStartDate
|
||||
subscriptionId String
|
||||
userId String @db.ObjectId
|
||||
}
|
||||
|
||||
model UserToken {
|
||||
id String @id @map("_id")
|
||||
created DateTime @db.Date
|
||||
ttl Float
|
||||
userId String @db.ObjectId
|
||||
}
|
||||
|
||||
model WebhookToken {
|
||||
id String @id @map("_id")
|
||||
created DateTime @db.Date
|
||||
ttl Float
|
||||
userId String @db.ObjectId
|
||||
}
|
||||
|
||||
model expressRateRecords {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
/// Field referred in an index, but found no data to define the type.
|
||||
expirationDate Json?
|
||||
|
||||
@@index([expirationDate], map: "expirationDate_1")
|
||||
}
|
||||
|
||||
model sessions {
|
||||
id String @id @map("_id")
|
||||
expires DateTime @db.Date
|
||||
session String
|
||||
|
||||
@@index([expires], map: "expires_1")
|
||||
}
|
||||
|
||||
model user {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
about String
|
||||
acceptedPrivacyTerms Boolean
|
||||
badges UserBadges
|
||||
completedChallenges UserCompletedChallenges[]
|
||||
currentChallengeId String
|
||||
email String
|
||||
/// Could not determine type: the field only had null or empty values in the sample set.
|
||||
emailAuthLinkTTL Json?
|
||||
emailVerified Boolean
|
||||
/// Could not determine type: the field only had null or empty values in the sample set.
|
||||
emailVerifyTTL Json?
|
||||
is2018DataVisCert Boolean
|
||||
is2018FullStackCert Boolean
|
||||
isApisMicroservicesCert Boolean
|
||||
isBackEndCert Boolean
|
||||
isBanned Boolean
|
||||
isCheater Boolean
|
||||
isDataAnalysisPyCertV7 Boolean
|
||||
isDataVisCert Boolean
|
||||
isDonating Boolean
|
||||
isFrontEndCert Boolean
|
||||
isFrontEndLibsCert Boolean
|
||||
isFullStackCert Boolean
|
||||
isHonest Boolean
|
||||
isInfosecCertV7 Boolean
|
||||
isInfosecQaCert Boolean
|
||||
isJsAlgoDataStructCert Boolean
|
||||
isMachineLearningPyCertV7 Boolean
|
||||
isQaCertV7 Boolean
|
||||
isRelationalDatabaseCertV8 Boolean
|
||||
isRespWebDesignCert Boolean
|
||||
isSciCompPyCertV7 Boolean
|
||||
keyboardShortcuts Boolean?
|
||||
location String
|
||||
name String
|
||||
picture String
|
||||
/// Could not determine type: the field only had null or empty values in the sample set.
|
||||
portfolio Json?
|
||||
profileUI UserProfileUi
|
||||
progressTimestamps Float[]
|
||||
rand Float
|
||||
sendQuincyEmail Boolean
|
||||
theme String
|
||||
username String
|
||||
usernameDisplay String?
|
||||
/// Could not determine type: the field only had null or empty values in the sample set.
|
||||
yearsTopContributor Json?
|
||||
}
|
||||
@@ -10,9 +10,63 @@ declare module 'fastify' {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this probably belongs in a separate file and may not be 100% correct.
|
||||
// All it's doing is providing the properties required by the current schema.
|
||||
const defaultUser = {
|
||||
about: '',
|
||||
acceptedPrivacyTerms: false,
|
||||
badges: {},
|
||||
completedChallenges: [],
|
||||
currentChallengeId: '',
|
||||
emailVerified: false,
|
||||
is2018DataVisCert: false,
|
||||
is2018FullStackCert: false,
|
||||
isApisMicroservicesCert: false,
|
||||
isBackEndCert: false,
|
||||
isBanned: false,
|
||||
isCheater: false,
|
||||
isDataAnalysisPyCertV7: false,
|
||||
isDataVisCert: false,
|
||||
isDonating: false,
|
||||
isFrontEndCert: false,
|
||||
isFrontEndLibsCert: false,
|
||||
isFullStackCert: false,
|
||||
isHonest: false,
|
||||
isInfosecCertV7: false,
|
||||
isInfosecQaCert: false,
|
||||
isJsAlgoDataStructCert: false,
|
||||
isMachineLearningPyCertV7: false,
|
||||
isQaCertV7: false,
|
||||
isRelationalDatabaseCertV8: false,
|
||||
isRespWebDesignCert: false,
|
||||
isSciCompPyCertV7: false,
|
||||
keyboardShortcuts: false,
|
||||
location: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
profileUI: {
|
||||
isLocked: false,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
},
|
||||
progressTimestamps: [],
|
||||
// TODO: check what this is used for in api-server and if we need it
|
||||
rand: 0,
|
||||
sendQuincyEmail: false,
|
||||
theme: 'default',
|
||||
// TODO: generate a UUID like in api-server
|
||||
username: ''
|
||||
};
|
||||
|
||||
export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
fastify.addHook('onRequest', fastify.authenticate);
|
||||
const collection = fastify.mongo.db?.collection('user');
|
||||
|
||||
fastify.get('/callback', async (req, _res) => {
|
||||
const auth0Res = await fetch(
|
||||
@@ -31,12 +85,17 @@ export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
}
|
||||
|
||||
const { email } = (await auth0Res.json()) as { email: string };
|
||||
const user = await collection?.findOne({ email });
|
||||
if (user) {
|
||||
req.session.user = { id: user._id.toString() };
|
||||
|
||||
const existingUser = await fastify.prisma.user.findFirst({
|
||||
where: { email }
|
||||
});
|
||||
if (existingUser) {
|
||||
req.session.user = { id: existingUser.id };
|
||||
} else {
|
||||
const DBRes = await collection?.insertOne({ email });
|
||||
req.session.user = { id: DBRes?.insertedId.toString() ?? '' };
|
||||
const newUser = await fastify.prisma.user.create({
|
||||
data: { ...defaultUser, email }
|
||||
});
|
||||
req.session.user = { id: newUser.id };
|
||||
}
|
||||
await req.session.save();
|
||||
});
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { ObjectId } from '@fastify/mongodb';
|
||||
import { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
const collection = fastify.mongo.db?.collection('user');
|
||||
|
||||
fastify.addHook('onRequest', fastify.authenticateSession);
|
||||
|
||||
fastify.get('/test', async (request, _reply) => {
|
||||
if (!collection) {
|
||||
return { error: 'No collection' };
|
||||
}
|
||||
const userId = new ObjectId(request.session.user.id);
|
||||
const user = await collection?.findOne({ _id: userId });
|
||||
return { user };
|
||||
});
|
||||
|
||||
fastify.put<{ Body: { quincyEmails: boolean } }>(
|
||||
'/update-privacy-terms',
|
||||
{
|
||||
@@ -28,27 +16,21 @@ export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
(req, res) => {
|
||||
async req => {
|
||||
const {
|
||||
body: { quincyEmails }
|
||||
} = req;
|
||||
|
||||
const update = {
|
||||
acceptedPrivacyTerms: true,
|
||||
sendQuincyEmail: !!quincyEmails
|
||||
};
|
||||
|
||||
const userId = new ObjectId(req.session.user.id);
|
||||
|
||||
return collection
|
||||
?.updateOne({ _id: userId }, { $set: update })
|
||||
.then(() => {
|
||||
void res.code(200).send({ msg: 'Successfully updated' });
|
||||
})
|
||||
.catch(err => {
|
||||
fastify.log.error(err);
|
||||
void res.code(500).send({ msg: 'Something went wrong' });
|
||||
try {
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: req.session.user.id },
|
||||
data: { acceptedPrivacyTerms: true, sendQuincyEmail: quincyEmails }
|
||||
});
|
||||
return { msg: 'Successfully updated' };
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
throw { msg: 'Something went wrong' };
|
||||
}
|
||||
}
|
||||
);
|
||||
done();
|
||||
|
||||
25
api/tools/docker-compose.yml
Normal file
25
api/tools/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
db:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
command: mongod --replSet rs0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 27018:27017
|
||||
volumes:
|
||||
- db-data:/data
|
||||
setup:
|
||||
image: mongo
|
||||
depends_on:
|
||||
- db
|
||||
restart: on-failure
|
||||
entrypoint: [
|
||||
'bash',
|
||||
'-c',
|
||||
# This will try to initiate the replica set, until it succeeds twice (i.e. until the replica set is already initialized)
|
||||
'mongosh --host db:27017 --eval ''try {rs.initiate();} catch (err) { if(err.codeName !== "AlreadyInitialized") throw err };'''
|
||||
]
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
@@ -29,7 +29,8 @@ if (process.env.NODE_ENV !== 'development') {
|
||||
}
|
||||
|
||||
export const MONGOHQ_URL =
|
||||
process.env.MONGOHQ_URL || 'mongodb://localhost:27017/freecodecamp';
|
||||
process.env.MONGOHQ_URL ??
|
||||
'mongodb://localhost:27017/freecodecamp?directConnection=true';
|
||||
export const NODE_ENV = process.env.NODE_ENV;
|
||||
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
|
||||
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
|
||||
|
||||
712
pnpm-lock.yaml
generated
712
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# Database
|
||||
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp
|
||||
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp?directConnection=true
|
||||
|
||||
# Logging
|
||||
SENTRY_DSN=dsn_from_sentry_dashboard
|
||||
|
||||
Reference in New Issue
Block a user