Add server
This commit is contained in:
commit
d883bf185b
|
|
@ -0,0 +1,3 @@
|
||||||
|
ProjectRouter - the actual routes to use for express
|
||||||
|
ProjectService - business logic
|
||||||
|
CrudService - data service used by business logic
|
||||||
|
|
@ -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" ]
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const uuidGenerator = () => {
|
||||||
|
return uuidv4();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uuidGenerator;
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue