mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): add prisma as orm (#49413)
This commit is contained in:
committed by
GitHub
parent
ca2086cacb
commit
fa7955dc75
@@ -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);
|
|
||||||
@@ -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;
|
||||||
+5
-2
@@ -9,8 +9,8 @@ import jwtAuthz from './plugins/fastify-jwt-authz';
|
|||||||
import sessionAuth from './plugins/session-auth';
|
import sessionAuth from './plugins/session-auth';
|
||||||
import { testRoutes } from './routes/test';
|
import { testRoutes } from './routes/test';
|
||||||
import { auth0Routes } from './routes/auth0';
|
import { auth0Routes } from './routes/auth0';
|
||||||
import { dbConnector } from './db';
|
|
||||||
import { testMiddleware } from './middleware';
|
import { testMiddleware } from './middleware';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AUTH0_AUDIENCE,
|
AUTH0_AUDIENCE,
|
||||||
AUTH0_DOMAIN,
|
AUTH0_DOMAIN,
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
SESSION_SECRET
|
SESSION_SECRET
|
||||||
} from './utils/env';
|
} from './utils/env';
|
||||||
|
|
||||||
|
import prismaPlugin from './db/prisma';
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
|
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
|
||||||
});
|
});
|
||||||
@@ -55,7 +57,8 @@ const start = async () => {
|
|||||||
|
|
||||||
void fastify.use('/test', testMiddleware);
|
void fastify.use('/test', testMiddleware);
|
||||||
|
|
||||||
void fastify.register(dbConnector);
|
void fastify.register(prismaPlugin);
|
||||||
|
|
||||||
void fastify.register(testRoutes);
|
void fastify.register(testRoutes);
|
||||||
void fastify.register(auth0Routes, { prefix: '/auth0' });
|
void fastify.register(auth0Routes, { prefix: '/auth0' });
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -6,9 +6,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^8.3.0",
|
"@fastify/cookie": "^8.3.0",
|
||||||
"@fastify/middie": "8.1",
|
"@fastify/middie": "8.1",
|
||||||
"@fastify/mongodb": "6.2.0",
|
|
||||||
"@fastify/session": "^10.1.1",
|
"@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": "4.14.1",
|
||||||
"fastify-auth0-verify": "^1.0.0",
|
"fastify-auth0-verify": "^1.0.0",
|
||||||
"fastify-plugin": "^4.3.0",
|
"fastify-plugin": "^4.3.0",
|
||||||
@@ -40,7 +40,12 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"develop": "nodemon index.ts",
|
"develop": "nodemon index.ts",
|
||||||
"start": "NODE_ENV=production node index.js",
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?
|
||||||
|
}
|
||||||
+65
-6
@@ -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) => {
|
export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
fastify.addHook('onRequest', fastify.authenticate);
|
fastify.addHook('onRequest', fastify.authenticate);
|
||||||
const collection = fastify.mongo.db?.collection('user');
|
|
||||||
|
|
||||||
fastify.get('/callback', async (req, _res) => {
|
fastify.get('/callback', async (req, _res) => {
|
||||||
const auth0Res = await fetch(
|
const auth0Res = await fetch(
|
||||||
@@ -31,12 +85,17 @@ export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { email } = (await auth0Res.json()) as { email: string };
|
const { email } = (await auth0Res.json()) as { email: string };
|
||||||
const user = await collection?.findOne({ email });
|
|
||||||
if (user) {
|
const existingUser = await fastify.prisma.user.findFirst({
|
||||||
req.session.user = { id: user._id.toString() };
|
where: { email }
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
req.session.user = { id: existingUser.id };
|
||||||
} else {
|
} else {
|
||||||
const DBRes = await collection?.insertOne({ email });
|
const newUser = await fastify.prisma.user.create({
|
||||||
req.session.user = { id: DBRes?.insertedId.toString() ?? '' };
|
data: { ...defaultUser, email }
|
||||||
|
});
|
||||||
|
req.session.user = { id: newUser.id };
|
||||||
}
|
}
|
||||||
await req.session.save();
|
await req.session.save();
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-28
@@ -1,20 +1,8 @@
|
|||||||
import { ObjectId } from '@fastify/mongodb';
|
|
||||||
import { FastifyPluginCallback } from 'fastify';
|
import { FastifyPluginCallback } from 'fastify';
|
||||||
|
|
||||||
export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
const collection = fastify.mongo.db?.collection('user');
|
|
||||||
|
|
||||||
fastify.addHook('onRequest', fastify.authenticateSession);
|
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 } }>(
|
fastify.put<{ Body: { quincyEmails: boolean } }>(
|
||||||
'/update-privacy-terms',
|
'/update-privacy-terms',
|
||||||
{
|
{
|
||||||
@@ -28,27 +16,21 @@ export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(req, res) => {
|
async req => {
|
||||||
const {
|
const {
|
||||||
body: { quincyEmails }
|
body: { quincyEmails }
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const update = {
|
try {
|
||||||
acceptedPrivacyTerms: true,
|
await fastify.prisma.user.update({
|
||||||
sendQuincyEmail: !!quincyEmails
|
where: { id: req.session.user.id },
|
||||||
};
|
data: { 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' });
|
|
||||||
});
|
});
|
||||||
|
return { msg: 'Successfully updated' };
|
||||||
|
} catch (err) {
|
||||||
|
fastify.log.error(err);
|
||||||
|
throw { msg: 'Something went wrong' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -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:
|
||||||
+2
-1
@@ -29,7 +29,8 @@ if (process.env.NODE_ENV !== 'development') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MONGOHQ_URL =
|
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 NODE_ENV = process.env.NODE_ENV;
|
||||||
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
|
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
|
||||||
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
|
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
|
||||||
|
|||||||
Generated
+368
-344
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Database
|
# Database
|
||||||
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp
|
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp?directConnection=true
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
SENTRY_DSN=dsn_from_sentry_dashboard
|
SENTRY_DSN=dsn_from_sentry_dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user