Add server

This commit is contained in:
Steven Fan 2024-05-05 11:44:02 -04:00
commit d883bf185b
14 changed files with 1837 additions and 0 deletions

3
server/.README Normal file
View File

@ -0,0 +1,3 @@
ProjectRouter - the actual routes to use for express
ProjectService - business logic
CrudService - data service used by business logic

24
server/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# pull official base image
FROM node:18.16.0
ARG STAGE
ENV STAGE $STAGE
# set working directory
WORKDIR /app
# Copies package.json and package-lock.json to Docker environment
COPY package*.json ./
# Installs all node packages
RUN npm install --legacy-peer-deps
# Copies everything over to Docker environment
COPY . .
# Uses port which is used by the actual application
EXPOSE 21288
# Run application
CMD [ "npm", "start" ]

1369
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
server/package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "tsc && node dist/app.js",
"dev": "node dist/app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/nano": "^7.0.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.0",
"lodash": "^4.17.21",
"nano": "^10.1.3",
"nodemon": "^2.0.22",
"ts-node": "^10.9.2",
"typescript": "^5.4.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/lodash": "^4.17.0",
"@types/node": "^20.11.30",
"@types/uuid": "^9.0.8"
}
}

33
server/src/app.ts Normal file
View File

@ -0,0 +1,33 @@
import bodyParser from "body-parser";
import cors from "cors";
import express from "express";
import nano, { ServerScope } from "nano";
import MemberRouter from "./routers/MemberRouter";
import { MEMBER_DB_ENDPOINT } from "./services/MemberService";
const app = express();
const PORT = process.env.PORT || 21288;
const corsConfiguration = {
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: "Content-Type"
};
app.use(cors(corsConfiguration));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
export const DB_URL = "https://sfan:awdasd131@couchdb.bv.newcovbap.church/";
const databaseServer: ServerScope = nano(DB_URL);
const routes = {
[MEMBER_DB_ENDPOINT]: MemberRouter(databaseServer)
};
Object.entries(routes).forEach(([route, router]) => app.use(`/${route}`, router));
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
export default app;

View File

@ -0,0 +1,69 @@
import express from "express";
import { ServerScope } from "nano";
import { MemberService } from "../services/MemberService";
import { log } from "../services/CrudService";
const MemberRouter = (databaseServer: ServerScope) => {
const memberService = MemberService(databaseServer);
const { getAll, getById, create, update, deleteById } = memberService;
const router = express.Router();
router.post("/", async (req, res) => {
try {
const newMember = req.body;
const response = await create(newMember);
res.json(response);
log("INFO", "created member");
} catch (error) {
res.status(500).json({ error });
}
});
router.get("/:id", async (req, res) => {
try {
const id = req.params.id;
const member = await getById(id);
res.json(member);
log("INFO", `get member ${id}`);
} catch (error) {
res.status(500).json({ error });
}
});
router.put("/", async (req, res) => {
try {
const id = req.body._id;
const memberData = req.body;
const response = await update(id, memberData);
res.json(response);
log("INFO", `update member ${id}`);
} catch (error) {
res.status(500).json({ error });
}
});
router.delete("/:id", async (req, res) => {
try {
const id = req.params.id;
const response = await deleteById(id);
res.json(response);
log("INFO", `delete member ${id}`);
} catch (error) {
res.status(500).json({ error });
}
});
router.get("/", async (req, res) => {
try {
const members = await getAll();
res.json(members);
log("INFO", `get all members`);
} catch (error) {
res.status(500).json({ error });
}
});
return router;
};
export default MemberRouter;

View File

@ -0,0 +1,76 @@
import { DocumentScope, MaybeDocument } from "nano";
type LOGTYPE = "INFO" | "WARNING" | "ERROR";
export function log(type: LOGTYPE, message: string): void {
const currentDateTime: string = new Date().toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short"
});
console.log(`[${type}] [${currentDateTime}] \t ${message}`);
}
export const CrudService = <T>(db: DocumentScope<T>) => {
async function create(document: T & MaybeDocument): Promise<any> {
try {
const response = await db.insert(document);
return response;
} catch (error) {
throw new Error("Error creating document: " + error);
}
}
async function getAll(): Promise<T[]> {
try {
const result = await db.list({ include_docs: true });
const documents = result.rows.map((row) => row.doc as T);
return documents;
} catch (error) {
throw new Error("Error getAll: " + error);
}
}
async function getById(id: string) {
try {
const document = await db.get(id);
return document;
} catch (error) {
throw new Error("Error getById: " + error);
}
}
async function update(documentId: string, documentData: any): Promise<any> {
try {
const user = await db.get(documentId); // Retrieve the user document from the database
const updatedUser = { _rev: user._rev, ...documentData };
const response = await db.insert(updatedUser, documentId);
return response;
} catch (error) {
throw new Error("Error updating document: " + error);
}
}
async function deleteById(id: string) {
try {
const fetchedDocument = await db.get(id);
const response = await db.destroy(id, fetchedDocument._rev);
return response;
} catch (error) {
console.error("Error deleting document ", error);
}
}
return {
getAll,
getById,
create,
update,
deleteById
};
};

View File

@ -0,0 +1,39 @@
import { ServerScope } from "nano";
import { Member } from "../types/member";
import { CrudService } from "./CrudService";
export const MEMBER_DB_ENDPOINT = "member_photos";
export const MemberService = (databaseServer: ServerScope) => {
const memberDatabase = databaseServer.use<Member>(MEMBER_DB_ENDPOINT);
const memberDataService = CrudService(memberDatabase);
function getAll() {
return memberDataService.getAll();
}
function getById(id: string) {
return memberDataService.getById(id);
}
function create(document: any) {
return memberDataService.create(document);
}
function update(_id: string, document: any) {
return memberDataService.update(_id, document);
}
// Delete a document by ID
function deleteById(id: string) {
return memberDataService.deleteById(id);
}
return {
getAll,
getById,
create,
update,
deleteById
};
};

View File

@ -0,0 +1,17 @@
import { Dictionary } from "lodash";
import { v4 as uuidv4 } from "uuid";
export type couchDocument = Dictionary<any> & {
_id: string;
};
export const createUUID = () => {
return uuidv4();
};
export function couchDocumentListToDictionary<T extends couchDocument>(objects: T[]): Dictionary<T> {
return objects.reduce((sum: Dictionary<T>, object: T) => {
if (object) sum[object._id] = object;
return sum;
}, {});
}

View File

@ -0,0 +1,72 @@
import { NumericDictionary } from "lodash";
import { couchDocument } from "./CouchDocument";
import uuidGenerator from "../utils/uuidGenerator";
export type Member = couchDocument & {
apt: string;
birthday: string;
city: string;
communityGroup: string;
email: string;
firstName: string;
houseNumber: string;
lastName: string;
marriedTo: string;
spouseIsMember: string;
memberSince: string;
memberStatus: string;
phone: string;
pictureFilename: string;
roles: string;
state: string;
streetName: string;
streetType: string;
userLogin: string;
zipCode: string;
image: string;
};
export const MemberUtils = {
getDefault: (): Member => ({
_id: uuidGenerator(),
apt: "",
birthday: "",
city: "",
communityGroup: "",
email: "",
firstName: "",
houseNumber: "",
lastName: "",
marriedTo: "",
spouseIsMember: "",
memberSince: "",
memberStatus: "",
phone: "",
pictureFilename: "",
roles: "",
state: "",
streetName: "",
streetType: "",
userLogin: "",
zipCode: "",
image: ""
}),
validate: (object: any): object is Member => "_id" in object,
inputs: (Member?: Member) => ({
name: {
name: "name",
label: "Member Name",
validationMessage: "Please enter the name of the Member",
inputProps: {
required: true,
defaultValue: Member?.name
}
}
}),
compareByName: (member: Member, other: Member) => {
return member.name.localeCompare(other.name);
}
};

View File

@ -0,0 +1,72 @@
// const monthNames = ["January", "February", "March", "April", "May", "June",
// "July", "August", "September", "October", "November", "December"
// ];
function convertMilitaryToRegularTime(militaryTime: string): string {
const [hours, minutes] = militaryTime.split(":");
let regularHours = parseInt(hours, 10);
let suffix = "AM";
if (regularHours >= 12) {
suffix = "PM";
if (regularHours > 12) {
regularHours -= 12;
}
}
const formattedMinutes = minutes.padStart(2, "0");
return `${regularHours}:${formattedMinutes} ${suffix}`;
}
function getFormattedNow() {
const currentDate: Date = new Date();
const hours: number = currentDate.getHours();
const minutes: number = currentDate.getMinutes();
// const seconds: number = currentDate.getSeconds();
const day: number = currentDate.getDate();
const month: string = currentDate.getMonth() + 1 + "";
const year: number = currentDate.getFullYear();
const regularTime: string = convertMilitaryToRegularTime(`${hours}:${minutes}`);
const formattedDate: string = `${month.padStart(2, "0")}/${day}/${year} - ${regularTime}`;
// console.log(formattedDate);
return formattedDate;
}
export function isFormattedNowFromToday(formattedNow: string): boolean {
return getDateFromFormattedNow(getFormattedNow()) === getDateFromFormattedNow(formattedNow);
}
export function getDateFromFormattedNow(formattedNow: string): string {
return formattedNow.slice(0, formattedNow.indexOf(" - "));
}
export function getTimeFromFormattedNow(formattedNow: string): string {
return formattedNow.slice(formattedNow.indexOf(" - ") + 3);
}
export default getFormattedNow;
export const dateFromString = (dateString: string) => {
const regex = /^(\d{1,2})\/(\d{1,2})\/(\d{4}) - (\d{1,2}):(\d{1,2}) (AM|PM)$/i;
const match = dateString.match(regex);
if (!match) {
throw new Error("Invalid date format");
}
const [, month, day, year, hours, minutes, amPm] = match;
const hour = amPm.toUpperCase() === "PM" && hours !== "12" ? parseInt(hours) + 12 : parseInt(hours);
const date = new Date(+year, +month - 1, +day, hour, parseInt(minutes));
return date;
};
export function getDisplayDateFromString(lastUpdated: string) {
return isFormattedNowFromToday(lastUpdated)
? "Today - " + getTimeFromFormattedNow(lastUpdated)
: getDateFromFormattedNow(lastUpdated);
}

View File

@ -0,0 +1,8 @@
export function countDigitsToRightOfDecimal(number: number): number {
const numberString = number.toString();
if (numberString.includes(".")) {
const decimalPart = numberString.split(".")[1];
return decimalPart.length;
}
return 0; // If there is no decimal point, return 0.
}

View File

@ -0,0 +1,7 @@
import { v4 as uuidv4 } from "uuid";
const uuidGenerator = () => {
return uuidv4();
};
export default uuidGenerator;

13
server/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2016",
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}