Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

Commit

Permalink
Merge pull request #72 from NavigoLearn/master
Browse files Browse the repository at this point in the history
Push to Staging
  • Loading branch information
sopyb authored Oct 25, 2023
2 parents 5c5e765 + 07a2445 commit c03c035
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "navigo-learn-api",
"version": "2.0.9",
"version": "2.0.10",
"description": "Navigo Learn API",
"repository": "https://github.com/NavigoLearn/API.git",
"author": "Navigo",
Expand Down
38 changes: 27 additions & 11 deletions src/controllers/roadmapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
responseRoadmapDeleted,
responseRoadmapNotFound,
responseRoadmapNotRated,
responseRoadmapProgressFound, responseRoadmapProgressNotFound,
responseRoadmapProgressFound,
responseRoadmapProgressNotFound,
responseRoadmapProgressUpdated,
responseRoadmapRated,
responseRoadmapUnrated,
Expand All @@ -25,13 +26,16 @@ import {
import {
deleteDBRoadmap,
deleteRoadmapLike,
getRoadmapLike,
getRoadmapObject,
getRoadmapLike, getRoadmapProgress,
getRoadmapProgress,
getUser,
insertRoadmap,
insertRoadmapLike,
insertRoadmapProgress,
updateRoadmap,
updateRoadmapLike, updateRoadmapProgress, insertRoadmapProgress,
updateRoadmapLike,
updateRoadmapProgress,
} from '@src/helpers/databaseManagement';
import { RequestWithBody } from '@src/middleware/validators/validateBody';
import { Roadmap, RoadmapTopic } from '@src/types/models/Roadmap';
Expand All @@ -40,6 +44,8 @@ import { addRoadmapView } from '@src/util/Views';
import logger from 'jet-logger';
import { RoadmapProgress } from '@src/types/models/RoadmapProgress';

const profanityCheckEnabled = false;

export async function createRoadmap(req: RequestWithBody, res: Response) {
// guaranteed to exist by middleware
const { name, description, data, miscData, version } = req.body;
Expand All @@ -57,7 +63,9 @@ export async function createRoadmap(req: RequestWithBody, res: Response) {
if (!topic || !Object.values(RoadmapTopic).includes(topic as RoadmapTopic))
topic = RoadmapTopic.PROGRAMMING;

const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled
? !Boolean(req.body.isprofane)
: true;
if (isDraft !== true && isDraft !== false) isDraft = false;
if (!isPublic) isDraft = true;

Expand Down Expand Up @@ -109,8 +117,8 @@ export async function getRoadmap(req: RequestWithSession, res: Response) {
1,
);
const isLiked =
userId !== undefined && userId !== null ?
await db.sumWhere(
userId !== undefined && userId !== null
? await db.sumWhere(
'roadmapLikes',
'value',
'roadmapId',
Expand Down Expand Up @@ -153,7 +161,9 @@ export async function updateAboutRoadmap(req: RequestWithBody, res: Response) {
if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic))
return responseInvalidBody(res);

const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled
? !Boolean(req.body.isprofane)
: roadmap.isPublic;

roadmap.set({
name: name as string,
Expand Down Expand Up @@ -193,7 +203,9 @@ export async function updateAllRoadmap(req: RequestWithBody, res: Response) {
if (!Object.values(RoadmapTopic).includes(topic as RoadmapTopic))
return responseInvalidBody(res);

const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled
? !Boolean(req.body.isprofane)
: roadmap.isPublic;
if (!isPublic) isDraft = true;

roadmap.set({
Expand Down Expand Up @@ -227,7 +239,7 @@ export async function updateNameRoadmap(req: RequestWithBody, res: Response) {
if (roadmap.userId !== userId) return responseNotAllowed(res);

const { name } = req.body;
const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled;

if (!name) return responseServerError(res);

Expand Down Expand Up @@ -261,7 +273,9 @@ export async function updateDescriptionRoadmap(
if (roadmap.userId !== userId) return responseNotAllowed(res);

const { description } = req.body;
const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled
? !Boolean(req.body.isprofane)
: roadmap.isPublic;

if (!description) return responseServerError(res);

Expand Down Expand Up @@ -292,7 +306,9 @@ export async function updateDataRoadmap(req: RequestWithBody, res: Response) {
if (roadmap.userId !== userId) return responseNotAllowed(res);

const { data } = req.body;
const isPublic = !Boolean(req.body.isProfane);
const isPublic = profanityCheckEnabled
? !Boolean(req.body.isprofane)
: roadmap.isPublic;

if (!data) return responseServerError(res);

Expand Down
15 changes: 15 additions & 0 deletions src/sql/migrations/0-example.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- example entry
-- please use {number} - {name} as your format to keep migrations in an consistent order
-- Top comment explaining why this is needed
-- 'migrations' table to track executed DB migrations
create table if not exists migrations
(
-- Each migration gets a unique ID
id BIGINT AUTO_INCREMENT PRIMARY KEY,

-- Timestamp of when the migration was executed
createdAt timestamp default current_timestamp() not null,

-- Name of the executed migration script
filename varchar(255) not null
)
10 changes: 9 additions & 1 deletion src/sql/setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,12 @@ select `navigo`.`sessionTable`.`id` AS `id`,
`navigo`.`sessionTable`.`token` AS `token`,
`navigo`.`sessionTable`.`expires` AS `expires`
from `navigo`.`sessionTable`
where `navigo`.`sessionTable`.`expires` >= current_timestamp();
where `navigo`.`sessionTable`.`expires` >= current_timestamp();

create table if not exists migrations
(
id BIGINT AUTO_INCREMENT
PRIMARY KEY,
createdAt timestamp default current_timestamp() not null,
filename varchar(255) not null
)
162 changes: 139 additions & 23 deletions src/util/Database/DatabaseDriver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import EnvVars from '@src/constants/EnvVars';
import { createPool, Pool } from 'mariadb';
import fs from 'fs';
import {createPool, Pool} from 'mariadb';
import path from 'path';
import logger from 'jet-logger';
import { User } from '@src/types/models/User';
import { GenericModelClass } from '@src/types/models/GenericModelClass';
import {User} from '@src/types/models/User';
import {GenericModelClass} from '@src/types/models/GenericModelClass';
import {readdirSync, readFileSync} from 'fs';

// database credentials
const { DBCred } = EnvVars;
Expand Down Expand Up @@ -59,6 +59,9 @@ const trustedColumns = [
// session
'token',
'expires',

// migrations
'filename',
];

// data interface
Expand Down Expand Up @@ -90,6 +93,12 @@ interface CountQueryPacket extends RowDataPacket {
result: bigint;
}

interface IMigrations {
id: bigint;
created_at: Date;
filename: string;
}

export interface ResultSetHeader {
fieldCount: number;
affectedRows: number;
Expand Down Expand Up @@ -418,6 +427,59 @@ class Database {
}
}

// TODO: less duplicate code
protected async _insertUnsafe(
table: string,
data: object | Record<string, DataType>,
discardId = true,
): Promise<bigint> {
const { keys, values } = processData(data, discardId);

// create sql query - insert into table (keys) values (values)
// ? for values to be replaced by params
const sql = `INSERT INTO ${table} (${keys.join(',')})
VALUES (${values.map(() => '?').join(',')})`;
// execute query
const result = (await this._queryUnsafe(sql, values)) as ResultSetHeader;

// return insert id
let insertId = -1n;
if (result) {
insertId = BigInt(result.insertId);
}
return insertId;
}

protected async getAllUnsafe<T>(table: string): Promise<T[] | null> {
// create sql query - select * from table
const sql = `SELECT *
FROM ${table}`;

// execute query
const result = await this._queryUnsafe(sql);

// check if T has any properties that are JSON
// if so parse them
return parseResult(result) as T[] | null;
}

protected async _queryUnsafe(
sql: string,
params?: unknown[],
): Promise<ResultSetHeader | RowDataPacket[]> {
if (!sql) return Promise.reject(new Error('No SQL query'));

// get connection from pool
const conn = await Database.pool.getConnection();
try {
// execute query and return result
return await conn.query(sql, params);
} finally {
// release connection
await conn.release();
}
}

protected async _getWhere<T>(
table: string,
like: boolean,
Expand Down Expand Up @@ -465,7 +527,7 @@ class Database {
const result = await this._query(sql, queryBuilderResult.params);

return BigInt(
(result as CountDataPacket[])[0][`SUM(${column})`] as number || 0,
((result as CountDataPacket[])[0][`SUM(${column})`] as number) || 0,
);
}

Expand All @@ -486,34 +548,62 @@ class Database {
}

protected async _setup() {
// get setup.sql file
let setupSql = fs.readFileSync(
path.join(__dirname, '..', '..', 'sql', 'setup.sql'),
'utf8',
);
const pathToSQLScripts = path.join(__dirname, '..', '..', 'sql');

// remove comments
setupSql = setupSql.replace(/--.*/g, '');
const numberRegex = /^\d+/gi;

// remove empty lines
setupSql = setupSql.replace(/^\s*[\r, \n]/gm, '');
// run the setup sql
await this._executeFile(path.join(pathToSQLScripts, 'setup.sql'));

// split sql queries
const queries = setupSql.split(';');
// read the migrations directory and sort files by number id
const migrationFiles = readdirSync(
path.join(pathToSQLScripts, 'migrations'),
)
.filter((file) => file.endsWith('.sql'))
.sort((fileA, fileB) => {
const numberA = parseInt(fileA.match(numberRegex)?.[0] || '');
const numberB = parseInt(fileB.match(numberRegex)?.[0] || '');

// get connection from pool
const conn = await Database.pool.getConnection();
return numberA - numberB;
});

try {
await this._executeFile(path.join(pathToSQLScripts, 'setup.sql'));
} catch (e) {
logger.err(e);
}

const migrations = await this.getAllUnsafe<IMigrations>('migrations');

if (!!migrations) {
migrations.forEach((migration) => {
const index = migrationFiles.indexOf(migration.filename);
if (index > -1) migrationFiles.splice(index, 1);
});
}

try {
// execute each query
for (const query of queries) {
if (query) await conn.query(query);
if (migrationFiles.length) logger.info('Starting migrations...');
for (const migrationFile of migrationFiles) {
logger.info(`Now running: ${migrationFile}...`);
await this._executeFile(
path.join(pathToSQLScripts, 'migrations', migrationFile),
);

const success = await this._insertUnsafe('migrations', {
id: -1n,
createdAt: new Date(),
filename: migrationFile,
});

if (success < 0n) {
throw new Error('Failed to insert migration file into database');
}
logger.info(`Successfully ran: ${migrationFile}`);
}
} catch (e) {
logger.err(e);
} finally {
// release connection
await conn.release();
Database.isSetup = true;
}

Expand All @@ -531,6 +621,32 @@ class Database {
} else await this.insert('users', user, false);
}

protected async _executeFile(path: string) {
let fileContent = readFileSync(path, {
encoding: 'utf8',
}).toString();

if (!fileContent) return;

// remove comments
fileContent = fileContent.replace(/--.*/g, '');

// remove empty lines
fileContent = fileContent.replace(/^\s*[\r, \n]/gm, '');

// split sql queries
const queries = fileContent.split(';');

// get connection from pool
const conn = await Database.pool.getConnection();

for (const query of queries) {
if (query) await conn.query(query);
}

await conn.release();
}

private _buildWhereQuery = (
like: boolean,
...values: unknown[]
Expand Down

0 comments on commit c03c035

Please sign in to comment.