Skip to content

Commit

Permalink
Merge pull request #4 from BYTE-Club-CCNY/Database
Browse files Browse the repository at this point in the history
Database
  • Loading branch information
LordFarquaadtheCreator authored Jun 11, 2024
2 parents 3811246 + ca6093d commit ae65518
Show file tree
Hide file tree
Showing 14 changed files with 623 additions and 150 deletions.
87 changes: 84 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,88 @@
# byte-server
# Byte-server
Server for byte

# Description
Server is built with Node.js and Express. Provides endpoints for managing a collection of projects, including creating, reading and updating. Exists both a json file and database backup for storing and quering projects
## Description
Server is built with Node.js and Express. Provides endpoints for managing a collection of projects, including creating, reading and updating. Exists both database and a local JSON file backup for storing and quering projects.

## How to use
Creating, Reading and Updating can be done from the database (default approach) endpoints. Only reading can be done for the local JSON file.

## Endpoints for DB

### Get projects
`/projects/get`

Retrieves all projects in the database and supports additional query params for filtering.

Filters include:
- **team**: TYPE string(s)
- **cohort**: TYPE string(s)
- **name**: TYPE string(s)

Filters are stackable, meaning you can do something similar to:
`/projects/get?team=John&cohort=1&team=Jane`
This will return all projects that have John **AND** Jane in their team and are in cohort 1. You can stack as many filters as you want but be aware that the more filters you add, the more specific the query will be, meaning it might return no results.

### Post new projects
`/projects/add`

**Please Fill Out All Fields!!!**
These fields must be provided as JSON in the body of the request.

The schema for projects is defined as follows:
- **name**: TYPE string(s)
- **"short-desc"**: TYPE string(s)
- **"long-desc"**: TYPE string(s)
- **team**: TYPE array of strings
- **link**: TYPE string(s)
- **Image**: TYPE string(s)
- **"tech-stack"**: TYPE array of strings
- **cohort**: TYPE string(s)
- **topic**: TYPE array of strings

### UPDATE projects
`/projects/update`

You **MUST** provide the project name you want to update as a query parameter like so:
`/projects/update?name={project_name}`

You can provide any key value pair you want to update in the body of the request, but it **HAS** to be JSON. This doesn't have to be all the fields, only the ones you want to update.

Reminder that the schema for projects is defined as follows:
- **name**: TYPE string(s)
- **"short-desc"**: TYPE string(s)
- **"long-desc"**: TYPE string(s)
- **team**: TYPE array of strings
- **link**: TYPE string(s)
- **Image**: TYPE string(s)
- **"tech-stack"**: TYPE array of strings
- **cohort**: TYPE string(s)
- **topic**: TYPE array of strings

Any mismatch of the schema types (i.e. providing a string when we expect an array) will return an error.

## Endpoints for Local File

### Get all projects
`/projects`

Retrieves all projects from the local JSON file and also supports additional query params for filtering.

### Get projects based on team members
`/projects?team={member_name}`

Replace `{member_name}` with a member that is in the project you would like to query. This filter is stackable, meaning you can do something similar to:
`/projects?team=John&team=Jane`

### Get projects based on cohort
`/projects?cohort={cohort}`

Replace `{cohort}` with the desired cohort to filter projects. This filter is stackable, meaning you can do something similar to:
`/projects?cohort=1&cohort=2`

### Get projects based on name
`/projects?name={project_name}`

Replace `{project_name}` with the desired project name to filter projects.
This filter is stackable, meaning you can do something similar to:
`/projects?name=Website&name=Server`
28 changes: 22 additions & 6 deletions data.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"image": "https://d1tzawfcgeew72.cloudfront.net/2048AI.png",
"tech-stack": ["Tkinter", "PyTorch", "NumPy"],
"cohort": "Fall 2025",
"topic": ["AI", "ML"]
"topic": [
"AI",
"ML"
],
"uuid": "80eb8a48-9172-4f6f-87ca-4ae89de1938c"
},
{
"name": "Stockbros",
Expand All @@ -19,7 +23,11 @@
"image": "https://d1tzawfcgeew72.cloudfront.net/old_byte.png",
"tech-stack": ["Pandas", "Plotly", "Flask", "React"],
"cohort": "Spring 2024",
"topic": ["Data Science", "Finance"]
"topic": [
"Data Science",
"Finance"
],
"uuid": "5dca7d6e-5131-434f-9eb9-5670a820a260"
},
{
"name": "TrackLeet",
Expand All @@ -35,7 +43,8 @@
"Chrome Extension",
"Web Development",
"Authentication"
]
],
"uuid": "e00bfd77-58a4-4d2f-a56e-6fc5b8280454"
},
{
"name": "Don't Be Alarmed",
Expand All @@ -51,7 +60,10 @@
"Android Studio"
],
"cohort": "Spring 2024",
"topic": ["Android App Development"]
"topic": [
"Android App Development"
],
"uuid": "9fe8f025-d0a4-4a88-abb5-184da85db827"
},
{
"name": "BYTE Website",
Expand All @@ -68,7 +80,10 @@
"Github Actions"
],
"cohort": "Summer 2024",
"topic": ["Web Development"]
"topic": [
"Web Development"
],
"uuid": "d78df4b2-4ad6-4c0e-82b1-31473c4ef9f2"
},
{
"name": "BYTE Server",
Expand All @@ -93,6 +108,7 @@
"Server Development",
"API",
"Postgres"
]
],
"uuid": "52a901bf-685a-4320-9336-5ce7785e4f18"
}
]
13 changes: 10 additions & 3 deletions db.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import dotenv from 'dotenv';
import {Client} from 'pg';
import dotenv from "dotenv";
import { Client } from "pg";
dotenv.config();

const client = new Client({
host: process.env.POSTGRESQL_DB_HOST,
user: process.env.POSTGRESQL_DB_USER,
password: process.env.POSTGRESQL_DB_PASSWORD,
database: process.env.POSTGRESQL_DB,
port: process.env.POSTGRESQL_DB_PORT ? parseInt(process.env.POSTGRESQL_DB_PORT) : 5432
port: process.env.POSTGRESQL_DB_PORT
? parseInt(process.env.POSTGRESQL_DB_PORT)
: 5432,
});

client.on("end", () => console.log("Client has disconnected"));
client.on("error", (err) =>
console.error("Unexpected error on idle client", err),
);

export default client;
32 changes: 18 additions & 14 deletions db.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import client from './db.config';
import client from './db.config';

const activateDb = async () => {
console.log("Connecting to Database ...");
let isActive = false;
const getDB = async () => {
console.log("Connecting to Database ...");
if (isActive) {
console.log("Database already connected");
return client;
}
try {
await client.connect();
console.log("Database connected");
isActive = true;
return client;
} catch (err: any) {
return client;
}
}

try {
await client.connect();
console.log("Database connected");

return client;
} catch (err: any) {
throw new Error(`Database connection error\n ${err.message}`);
}
}

export default activateDb;
export default getDB;
22 changes: 22 additions & 0 deletions dbCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import client from "./db.config";

async function getDB() {
await client.connect();
}

async function checkDB(timeout: number) {
setTimeout(async () => {
try {
await getDB();
process.exit(0);
} catch (e: any) {
console.error("Error Connecting to DB:", e.message);
process.exit(1);
}
}, timeout);
}

const args: string[] = process.argv;
const timeout: number = parseInt(args[2]);

await checkDB(timeout);
34 changes: 34 additions & 0 deletions dbChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { spawn } from "child_process";

export function secondsToMs(d: number) {
return d * 1000;
}

// threading should happen at top level of server "setInterval"
async function checkDB(TIMEOUT: number): Promise<boolean> {
return new Promise((resolve, reject) => {
let dbAval: boolean = false;

const database = spawn("bun", ["dbCheck.ts", TIMEOUT.toString()]);

database.stdout.on("data", (data) => {
console.log("Output from dbCheck.ts:", data.toString());
});

database.on("exit", (code) => {
if (code === 0) {
dbAval = true;
} else {
dbAval = false;
}
resolve(dbAval);
});

database.on("error", (error) => {
console.error(error);
reject(error);
});
});
}

export default checkDB;
14 changes: 14 additions & 0 deletions middlewares/connectDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import getDB from "../db";
import logger from "../utils/logger";

const connectDB = async (req: any, res: any, next: any) => {
try {
req.client = await getDB();
next();
} catch (err: any) {
logger.info(err.message);
next("route");
}
}

export default connectDB;
81 changes: 81 additions & 0 deletions middlewares/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//validate all fields and their types
function validating(
keys: string[],
values: any,
requiredFields: { [key: string]: string },
res: any,
) {
for (let i = 0; i < values.length; i++) {
//initial check if the field is a required field
if (!(keys[i] in requiredFields)) {
return res
.status(400)
.json({
message: `Please insert a valid field name, ${keys[i]} is invalid`,
});
}
//check for the correct typing
if (typeof values[i] !== requiredFields[keys[i]]) {
return res
.status(400)
.json({
message: `Please insert the correct typing for ${keys[i]}, it should be a ${requiredFields[keys[i]] === "object" ? "array of strings" : requiredFields[keys[i]]}!`,
});
}
}
// if no response is sent meaning all validations passed, return false at the end of the function
return false;
}

const validate = (req: any, res: any, next: any) => {
const requiredFields = {
name: "string",
"short-desc": "string",
"long-desc": "string",
team: "object",
link: "string",
image: "string",
"tech-stack": "object",
cohort: "string",
topic: "object",
};
const values = Object.values(req.body);
const keys = Object.keys(req.body);

if (req.method === "POST") {
//initial check for empty request body
if (Object.keys(req.body).length === 0) {
return res
.status(400)
.json({
message: "Please insert a object with all required fields!",
});
}
if (keys.length !== 9) {
return res
.status(400)
.json({
message:
"Please insert all required fields, you are missing some fields!",
});
} else {
if (validating(keys, values, requiredFields, res)) {
return;
}
}
} else {
//initial check for empty request body
if (Object.keys(req.body).length === 0) {
return res
.status(400)
.json({ message: "Please insert a object to update!" });
} else {
if (validating(keys, values, requiredFields, res)) {
return;
}
}
}
next();
};

export default validate;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@types/chai": "^4.3.16",
"@types/chai-http": "^4.2.0",
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.21",
"@types/mocha": "^10.0.6",
Expand Down
Loading

0 comments on commit ae65518

Please sign in to comment.