diff --git a/package-lock.json b/package-lock.json index df5c8f0..6eb54c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", + "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", + "moment": "^2.30.1", "ms": "^2.1.3", "mysql": "^2.18.1", "node-schedule": "^2.1.1", @@ -1822,6 +1824,17 @@ "node": ">=0.10.0" } }, + "node_modules/ics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz", + "integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==", + "license": "ISC", + "dependencies": { + "nanoid": "^3.1.23", + "runes2": "^1.1.2", + "yup": "^1.2.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2217,6 +2230,15 @@ "optional": true, "peer": true }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2252,6 +2274,24 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -2597,6 +2637,12 @@ "node": ">=0.4.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2823,6 +2869,12 @@ "node": ">= 0.10" } }, + "node_modules/runes2": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", + "integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3366,6 +3418,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3374,6 +3432,12 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -3455,6 +3519,18 @@ "license": "Unlicense", "optional": true }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3918,6 +3994,18 @@ "engines": { "node": ">=6" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/package.json b/package.json index 12c3fbd..4190f17 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", + "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", + "moment": "^2.30.1", "ms": "^2.1.3", "mysql": "^2.18.1", "node-schedule": "^2.1.1", diff --git a/src/command/calendarCommand.ts b/src/command/calendarCommand.ts new file mode 100644 index 0000000..e6365db --- /dev/null +++ b/src/command/calendarCommand.ts @@ -0,0 +1,24 @@ +export interface CreateCalendarCommand { + starttime: Date; + endtime: Date; + title: string; + content: string; + location: string; + allDay: boolean; + typeId: number; +} + +export interface UpdateCalendarCommand { + id: string; + starttime: Date; + endtime: Date; + title: string; + content: string; + location: string; + allDay: boolean; + typeId: number; +} + +export interface DeleteCalendarCommand { + id: string; +} diff --git a/src/command/calendarCommandHandler.ts b/src/command/calendarCommandHandler.ts new file mode 100644 index 0000000..9592347 --- /dev/null +++ b/src/command/calendarCommandHandler.ts @@ -0,0 +1,96 @@ +import { dataSource } from "../data-source"; +import { calendar } from "../entity/calendar"; +import { calendarType } from "../entity/calendarType"; +import InternalException from "../exceptions/internalException"; +import { CreateCalendarCommand, DeleteCalendarCommand, UpdateCalendarCommand } from "./calendarCommand"; + +export default abstract class CalendarCommandHandler { + /** + * @description create calendar + * @param CreateCalendarCommand + * @returns {Promise} + */ + static async create(createCalendar: CreateCalendarCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(calendar) + .values({ + starttime: createCalendar.starttime, + endtime: createCalendar.endtime, + title: createCalendar.title, + content: createCalendar.content, + location: createCalendar.location, + allDay: createCalendar.allDay, + type: await dataSource + .getRepository(calendarType) + .createQueryBuilder("type") + .where("id = :id", { id: createCalendar.typeId }) + .getOneOrFail(), + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed creating calendar", err); + }); + } + + /** + * @description update calendar + * @param UpdateCalendarCommand + * @returns {Promise} + */ + static async update(updateCalendar: UpdateCalendarCommand): Promise { + let sequence = await dataSource + .getRepository(calendar) + .createQueryBuilder("calendar") + .where("id = :id", { id: updateCalendar.id }) + .getOneOrFail() + .then((res) => { + return res.sequence; + }); + return await dataSource + .createQueryBuilder() + .update(calendar) + .set({ + starttime: updateCalendar.starttime, + endtime: updateCalendar.endtime, + title: updateCalendar.title, + content: updateCalendar.content, + location: updateCalendar.location, + allDay: updateCalendar.allDay, + type: await dataSource + .getRepository(calendarType) + .createQueryBuilder("type") + .where("id = :id", { id: updateCalendar.typeId }) + .getOneOrFail(), + sequence: sequence + 1, + }) + .where("id = :id", { id: updateCalendar.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating award", err); + }); + } + + /** + * @description delete calendar + * @param DeleteCalendarCommand + * @returns {Promise} + */ + static async delete(deleteCalendar: DeleteCalendarCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(calendar) + .where("id = :id", { id: deleteCalendar.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed deleting calendar", err); + }); + } +} diff --git a/src/command/calendarTypeCommand.ts b/src/command/calendarTypeCommand.ts new file mode 100644 index 0000000..fdab618 --- /dev/null +++ b/src/command/calendarTypeCommand.ts @@ -0,0 +1,16 @@ +export interface CreateCalendarTypeCommand { + type: string; + nscdr: boolean; + color: string; +} + +export interface UpdateCalendarTypeCommand { + id: number; + type: string; + nscdr: boolean; + color: string; +} + +export interface DeleteCalendarTypeCommand { + id: number; +} diff --git a/src/command/calendarTypeCommandHandler.ts b/src/command/calendarTypeCommandHandler.ts new file mode 100644 index 0000000..0425425 --- /dev/null +++ b/src/command/calendarTypeCommandHandler.ts @@ -0,0 +1,70 @@ +import { dataSource } from "../data-source"; +import { calendarType } from "../entity/calendarType"; +import InternalException from "../exceptions/internalException"; +import { CreateCalendarTypeCommand, DeleteCalendarTypeCommand, UpdateCalendarTypeCommand } from "./calendarTypeCommand"; + +export default abstract class CalendarTypeCommandHandler { + /** + * @description create calendarType + * @param CreateCalendarTypeCommand + * @returns {Promise} + */ + static async create(createCalendarType: CreateCalendarTypeCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(calendarType) + .values({ + type: createCalendarType.type, + nscdr: createCalendarType.nscdr, + color: createCalendarType.color, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed creating calendarType", err); + }); + } + + /** + * @description update calendarType + * @param UpdateCalendarTypeCommand + * @returns {Promise} + */ + static async update(updateCalendarType: UpdateCalendarTypeCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(calendarType) + .set({ + type: updateCalendarType.type, + nscdr: updateCalendarType.nscdr, + color: updateCalendarType.color, + }) + .where("id = :id", { id: updateCalendarType.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating award", err); + }); + } + + /** + * @description delete calendarType + * @param DeleteCalendarTypeCommand + * @returns {Promise} + */ + static async delete(deleteCalendarType: DeleteCalendarTypeCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(calendarType) + .where("id = :id", { id: deleteCalendarType.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed deleting calendarType", err); + }); + } +} diff --git a/src/controller/admin/calendarController.ts b/src/controller/admin/calendarController.ts new file mode 100644 index 0000000..0b319b1 --- /dev/null +++ b/src/controller/admin/calendarController.ts @@ -0,0 +1,201 @@ +import { Request, Response } from "express"; +import CalendarService from "../../service/calendarService"; +import CalendarFactory from "../../factory/admin/calendar"; +import CalendarTypeService from "../../service/calendarTypeService"; +import CalendarTypeFactory from "../../factory/admin/calendarType"; +import { CreateCalendarCommand, DeleteCalendarCommand, UpdateCalendarCommand } from "../../command/calendarCommand"; +import CalendarCommandHandler from "../../command/calendarCommandHandler"; +import { + CreateCalendarTypeCommand, + DeleteCalendarTypeCommand, + UpdateCalendarTypeCommand, +} from "../../command/calendarTypeCommand"; +import CalendarTypeCommandHandler from "../../command/calendarTypeCommandHandler"; + +/** + * @description get all calendar items + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllCalendarItems(req: Request, res: Response): Promise { + let items = await CalendarService.getAll(); + + res.json(CalendarFactory.mapToBase(items)); +} + +/** + * @description get calendar item by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getCalendarItemById(req: Request, res: Response): Promise { + const id = req.params.id; + let item = await CalendarService.getById(id); + + res.json(CalendarFactory.mapToSingle(item)); +} + +/** + * @description get all calendar types + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllCalendarTypes(req: Request, res: Response): Promise { + let types = await CalendarTypeService.getAll(); + + res.json(CalendarTypeFactory.mapToBase(types)); +} + +/** + * @description get calendar type by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getCalendarTypeById(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let type = await CalendarTypeService.getById(id); + + res.json(CalendarTypeFactory.mapToSingle(type)); +} + +/** + * @description create calendar item + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createCalendarItem(req: Request, res: Response): Promise { + const starttime = req.body.starttime; + const endtime = req.body.endtime; + const title = req.body.title; + const content = req.body.content; + const location = req.body.location; + const allDay = req.body.allDay; + const typeId = req.body.typeId; + + let createItem: CreateCalendarCommand = { + starttime, + endtime, + title, + content, + location, + allDay, + typeId, + }; + let id = await CalendarCommandHandler.create(createItem); + + res.send(id); +} + +/** + * @description create calendar type + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createCalendarType(req: Request, res: Response): Promise { + const type = req.body.type; + const nscdr = req.body.nscdr; + const color = req.body.color; + + let createType: CreateCalendarTypeCommand = { + type, + nscdr, + color, + }; + let id = await CalendarTypeCommandHandler.create(createType); + + res.send(id); +} + +/** + * @description update calendar item + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateCalendarItem(req: Request, res: Response): Promise { + const id = req.params.id; + const starttime = req.body.starttime; + const endtime = req.body.endtime; + const title = req.body.title; + const content = req.body.content; + const location = req.body.location; + const allDay = req.body.allDay; + const typeId = req.body.typeId; + + let updateItem: UpdateCalendarCommand = { + id, + starttime, + endtime, + title, + content, + location, + allDay, + typeId, + }; + await CalendarCommandHandler.update(updateItem); + + res.sendStatus(204); +} + +/** + * @description update calendar type + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateCalendarType(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + const type = req.body.type; + const nscdr = req.body.nscdr; + const color = req.body.color; + + let updateType: UpdateCalendarTypeCommand = { + id, + type, + nscdr, + color, + }; + await CalendarTypeCommandHandler.update(updateType); + + res.sendStatus(204); +} + +/** + * @description delete calendar item + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteCalendarItem(req: Request, res: Response): Promise { + const id = req.params.id; + + let deleteItem: DeleteCalendarCommand = { + id, + }; + await CalendarCommandHandler.delete(deleteItem); + + res.sendStatus(204); +} + +/** + * @description delete calendar type + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteCalendarType(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let deleteType: DeleteCalendarTypeCommand = { + id, + }; + await CalendarTypeCommandHandler.delete(deleteType); + + res.sendStatus(204); +} diff --git a/src/controller/publicController.ts b/src/controller/publicController.ts new file mode 100644 index 0000000..ef675fa --- /dev/null +++ b/src/controller/publicController.ts @@ -0,0 +1,66 @@ +import { Request, Response } from "express"; +import CalendarService from "../service/calendarService"; +import CalendarTypeService from "../service/calendarTypeService"; +import { calendar } from "../entity/calendar"; +import { createEvents } from "ics"; +import moment from "moment"; + +/** + * @description get all calendar items by types or nscdr + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getCalendarItemsByTypes(req: Request, res: Response): Promise { + let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types]; + + let items: Array = []; + if (types.length == 0) { + let typeIds = await CalendarTypeService.getByTypes(types as Array); + items = await CalendarService.getByTypes(typeIds.map((t) => t.id)); + } else { + items = await CalendarService.getByTypeNSCDR(); + } + + let events = createEvents( + items.map((i) => ({ + calName: process.env.CLUB_NAME, + uid: i.id, + sequence: 1, + start: moment(i.starttime) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + end: moment(i.endtime) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + title: i.title, + description: i.content, + location: i.location, + categories: [i.type.type], + created: moment(i.createdAt) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + lastModified: moment(i.updatedAt) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + transp: "OPAQUE" as "OPAQUE", + url: "https://www.ff-merching.de", + alarms: [ + { + action: "display", + description: "Erinnerung", + trigger: { + minutes: 30, + before: true, + }, + }, + ], + })) + ); + + res.type("ics").send(events.value); +} diff --git a/src/data-source.ts b/src/data-source.ts index cf9058c..a38def0 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -36,6 +36,9 @@ import { protocolPresence } from "./entity/protocolPresence"; import { protocolVoting } from "./entity/protocolVoting"; import { protocolPrintout } from "./entity/protocolPrintout"; import { Protocol1729347911107 } from "./migrations/1729347911107-protocol"; +import { calendar } from "./entity/calendar"; +import { calendarType } from "./entity/calendarType"; +import { Calendar1729947763295 } from "./migrations/1729947763295-calendar"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -71,6 +74,8 @@ const dataSource = new DataSource({ protocolPresence, protocolVoting, protocolPrintout, + calendar, + calendarType, ], migrations: [ Initial1724317398939, @@ -82,6 +87,7 @@ const dataSource = new DataSource({ Memberdata1726301836849, CommunicationFields1727439800630, Protocol1729347911107, + Calendar1729947763295, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/calendar.ts b/src/entity/calendar.ts new file mode 100644 index 0000000..041a998 --- /dev/null +++ b/src/entity/calendar.ts @@ -0,0 +1,52 @@ +import { + Column, + Entity, + ManyToOne, + PrimaryColumn, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + AfterUpdate, + BeforeUpdate, +} from "typeorm"; +import { calendarType } from "./calendarType"; + +@Entity() +export class calendar { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ type: "datetime", nullable: false }) + starttime: Date; + + @Column({ type: "datetime", nullable: false }) + endtime: Date; + + @Column({ type: "varchar", length: 255, nullable: false }) + title: string; + + @Column({ type: "text", nullable: true }) + content: string; + + @Column({ type: "text", nullable: true }) + location: string; + + @Column({ type: "boolean", default: false }) + allDay: boolean; + + @Column({ type: "int", default: 1 }) + sequence: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @ManyToOne(() => calendarType, (t) => t.calendar, { + nullable: false, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + type: calendarType; +} diff --git a/src/entity/calendarType.ts b/src/entity/calendarType.ts new file mode 100644 index 0000000..5f696cc --- /dev/null +++ b/src/entity/calendarType.ts @@ -0,0 +1,24 @@ +import { Column, Entity, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; +import { calendar } from "./calendar"; + +@Entity() +export class calendarType { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255 }) + type: string; + + @Column({ type: "boolean" }) // none specified cal dav request + nscdr: boolean; + + @Column({ type: "varchar", length: 255 }) + color: string; + + @OneToMany(() => calendar, (c) => c.type, { + nullable: false, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + calendar: calendar[]; +} diff --git a/src/factory/admin/calendar.ts b/src/factory/admin/calendar.ts new file mode 100644 index 0000000..aa29be2 --- /dev/null +++ b/src/factory/admin/calendar.ts @@ -0,0 +1,34 @@ +import { calendar } from "../../entity/calendar"; +import { CalendarViewModel } from "../../viewmodel/admin/calendar.models"; +import CalendarTypeFactory from "./calendarType"; + +export default abstract class CalendarFactory { + /** + * @description map record to calendar + * @param {calendar} record + * @returns {CalendarViewModel} + */ + public static mapToSingle(record: calendar): CalendarViewModel { + return { + id: record.id, + starttime: record.starttime, + endtime: record.endtime, + title: record.title, + content: record.content, + location: record.location, + allDay: record.allDay, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + type: CalendarTypeFactory.mapToSingle(record.type), + }; + } + + /** + * @description map records to calendar + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/calendarType.ts b/src/factory/admin/calendarType.ts new file mode 100644 index 0000000..d1f6850 --- /dev/null +++ b/src/factory/admin/calendarType.ts @@ -0,0 +1,27 @@ +import { calendarType } from "../../entity/calendarType"; +import { CalendarTypeViewModel } from "../../viewmodel/admin/calendarType.models"; + +export default abstract class CalendarTypeFactory { + /** + * @description map record to calendarType + * @param {calendarType} record + * @returns {CalendarTypeViewModel} + */ + public static mapToSingle(record: calendarType): CalendarTypeViewModel { + return { + id: record.id, + type: record.type, + nscdr: record.nscdr, + color: record.color, + }; + } + + /** + * @description map records to calendarType + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/migrations/1729947763295-calendar.ts b/src/migrations/1729947763295-calendar.ts new file mode 100644 index 0000000..bd30062 --- /dev/null +++ b/src/migrations/1729947763295-calendar.ts @@ -0,0 +1,68 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; +import { DB_TYPE } from "../env.defaults"; + +export class Calendar1729947763295 implements MigrationInterface { + name = "Calendar1729947763295"; + + public async up(queryRunner: QueryRunner): Promise { + const variableType_int = DB_TYPE == "mysql" ? "int" : "integer"; + + await queryRunner.createTable( + new Table({ + name: "calendar_type", + columns: [ + { name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" }, + { name: "type", type: "varchar", length: "255", isNullable: false }, + { name: "nscdr", type: "tinyint", isNullable: false }, + { name: "color", type: "varchar", length: "255", isNullable: false }, + ], + }) + ); + + await queryRunner.createTable( + new Table({ + name: "calendar", + columns: [ + { name: "id", type: "varchar", length: "36", isPrimary: true, isGenerated: true, generationStrategy: "uuid" }, + { name: "starttime", type: "datetime", isNullable: false }, + { name: "endtime", type: "datetime", isNullable: false }, + { name: "title", type: "varchar", length: "255", isNullable: false }, + { name: "content", type: "text", isNullable: true }, + { name: "allDay", type: "tinyint", isNullable: false, default: 0 }, + { name: "location", type: "text", isNullable: true }, + { name: "sequence", type: variableType_int, default: 1 }, + { name: "createdAt", type: "datetime", precision: 6, isNullable: false, default: "CURRENT_TIMESTAMP(6)" }, + { + name: "updatedAt", + type: "datetime", + precision: 6, + isNullable: false, + default: "CURRENT_TIMESTAMP(6)", + onUpdate: "CURRENT_TIMESTAMP(6)", + }, + { name: "typeId", type: variableType_int, isNullable: false }, + ], + }) + ); + + await queryRunner.createForeignKey( + "calendar", + new TableForeignKey({ + columnNames: ["typeId"], + referencedColumnNames: ["id"], + referencedTableName: "calendar_type", + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const table = await queryRunner.getTable("calendar"); + const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("typeId") !== -1); + await queryRunner.dropForeignKey("calendar", foreignKey); + + await queryRunner.dropTable("calendar"); + await queryRunner.dropTable("calendar_type"); + } +} diff --git a/src/routes/admin/calendar.ts b/src/routes/admin/calendar.ts new file mode 100644 index 0000000..e6f7181 --- /dev/null +++ b/src/routes/admin/calendar.ts @@ -0,0 +1,98 @@ +import express, { Request, Response } from "express"; +import { + getCalendarItemById, + getAllCalendarItems, + getAllCalendarTypes, + getCalendarTypeById, + createCalendarItem, + createCalendarType, + updateCalendarItem, + updateCalendarType, + deleteCalendarItem, + deleteCalendarType, +} from "../../controller/admin/calendarController"; +import PermissionHelper from "../../helpers/permissionHelper"; + +var router = express.Router({ mergeParams: true }); + +router.get( + "/items", + PermissionHelper.passCheckMiddleware("read", "club", "calendar"), + async (req: Request, res: Response) => { + await getAllCalendarItems(req, res); + } +); + +router.get( + "/item/:id", + PermissionHelper.passCheckMiddleware("read", "club", "calendar"), + async (req: Request, res: Response) => { + await getCalendarItemById(req, res); + } +); + +router.get( + "/types", + PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), + async (req: Request, res: Response) => { + await getAllCalendarTypes(req, res); + } +); + +router.get( + "/type/:id", + PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), + async (req: Request, res: Response) => { + await getCalendarTypeById(req, res); + } +); + +router.post( + "/item", + PermissionHelper.passCheckMiddleware("create", "club", "calendar"), + async (req: Request, res: Response) => { + await createCalendarItem(req, res); + } +); + +router.post( + "/type", + PermissionHelper.passCheckMiddleware("create", "settings", "calendar_type"), + async (req: Request, res: Response) => { + await createCalendarType(req, res); + } +); + +router.patch( + "/item/:id", + PermissionHelper.passCheckMiddleware("update", "club", "calendar"), + async (req: Request, res: Response) => { + await updateCalendarItem(req, res); + } +); + +router.patch( + "/type/:id", + PermissionHelper.passCheckMiddleware("update", "settings", "calendar_type"), + async (req: Request, res: Response) => { + await updateCalendarType(req, res); + } +); + +router.delete( + "/item/:id", + PermissionHelper.passCheckMiddleware("delete", "club", "calendar"), + async (req: Request, res: Response) => { + await deleteCalendarItem(req, res); + } +); + +router.delete( + "/type/:id", + PermissionHelper.passCheckMiddleware("delete", "settings", "calendar_type"), + async (req: Request, res: Response) => { + await deleteCalendarType(req, res); + } +); + +export default router; diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index 6c891e1..cdafa5e 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -10,6 +10,8 @@ import qualification from "./qualification"; import member from "./member"; import protocol from "./protocol"; +import calendar from "./calendar"; + import role from "./role"; import user from "./user"; @@ -36,6 +38,7 @@ router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "setti router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member); router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol); +router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "calendar"), calendar); router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role); router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user); diff --git a/src/routes/index.ts b/src/routes/index.ts index 2dcff79..070508c 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -6,6 +6,7 @@ import allowSetup from "../middleware/allowSetup"; import authenticate from "../middleware/authenticate"; import errorHandler from "../middleware/errorHandler"; +import publicAvailable from "./public"; import setup from "./setup"; import auth from "./auth"; import admin from "./admin/index"; @@ -21,6 +22,7 @@ export default (app: Express) => { app.use(cors()); app.options("*", cors()); + app.use("/public", publicAvailable); app.use("/setup", allowSetup, setup); app.use("/auth", auth); app.use(authenticate); diff --git a/src/routes/public.ts b/src/routes/public.ts new file mode 100644 index 0000000..56b4784 --- /dev/null +++ b/src/routes/public.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { getCalendarItemsByTypes } from "../controller/publicController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/calendar", async (req, res) => { + await getCalendarItemsByTypes(req, res); +}); + +export default router; diff --git a/src/service/calendarService.ts b/src/service/calendarService.ts new file mode 100644 index 0000000..48c771d --- /dev/null +++ b/src/service/calendarService.ts @@ -0,0 +1,80 @@ +import { dataSource } from "../data-source"; +import { calendar } from "../entity/calendar"; +import InternalException from "../exceptions/internalException"; + +export default abstract class CalendarService { + /** + * @description get all calendars + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(calendar) + .createQueryBuilder("calendar") + .leftJoinAndSelect("calendar.type", "type") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendars not found", err); + }); + } + + /** + * @description get calendar by id + * @returns {Promise} + */ + static async getById(id: string): Promise { + return await dataSource + .getRepository(calendar) + .createQueryBuilder("calendar") + .leftJoinAndSelect("calendar.type", "type") + .where("calendar.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendar not found by id", err); + }); + } + + /** + * @description get calendar by types + * @returns {Promise>} + */ + static async getByTypes(types: Array): Promise> { + return await dataSource + .getRepository(calendar) + .createQueryBuilder("calendar") + .leftJoinAndSelect("calendar.type", "type") + .where("type.id IN (:...types)", { types: types }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendars not found by types", err); + }); + } + + /** + * @description get calendar by types nscdr + * @returns {Promise>} + */ + static async getByTypeNSCDR(): Promise> { + return await dataSource + .getRepository(calendar) + .createQueryBuilder("calendar") + .leftJoinAndSelect("calendar.type", "type") + .where("type.nscdr = :nscdr", { nscdr: true }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendars not found by type nscdr", err); + }); + } +} diff --git a/src/service/calendarTypeService.ts b/src/service/calendarTypeService.ts new file mode 100644 index 0000000..08e76ea --- /dev/null +++ b/src/service/calendarTypeService.ts @@ -0,0 +1,58 @@ +import { dataSource } from "../data-source"; +import { calendarType } from "../entity/calendarType"; +import InternalException from "../exceptions/internalException"; + +export default abstract class CalendarTypeService { + /** + * @description get all calendar types + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(calendarType) + .createQueryBuilder("calendarType") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendarTypes not found", err); + }); + } + + /** + * @description get calendar type by id + * @returns {Promise} + */ + static async getById(id: number): Promise { + return await dataSource + .getRepository(calendarType) + .createQueryBuilder("calendarType") + .where("calendarType.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendarType not found by id", err); + }); + } + + /** + * @description get calendar by names + * @returns {Promise>} + */ + static async getByTypes(names: Array): Promise> { + return await dataSource + .getRepository(calendarType) + .createQueryBuilder("calendarType") + .where("calendarType.type IN (:...names)", { names: names }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("calendarTypes not found by names", err); + }); + } +} diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index cb8a989..fc468a9 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -10,6 +10,7 @@ export type PermissionModule = | "executive_position" | "communication" | "membership_status" + | "calendar_type" | "user" | "role"; @@ -45,12 +46,13 @@ export const permissionModules: Array = [ "executive_position", "communication", "membership_status", + "calendar_type", "user", "role", ]; export const permissionTypes: Array = ["read", "create", "update", "delete"]; export const sectionsAndModules: SectionsAndModulesObject = { club: ["member", "calendar", "newsletter", "protocol"], - settings: ["qualification", "award", "executive_position", "communication", "membership_status"], + settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"], user: ["user", "role"], }; diff --git a/src/viewmodel/admin/calendar.models.ts b/src/viewmodel/admin/calendar.models.ts new file mode 100644 index 0000000..b65fb88 --- /dev/null +++ b/src/viewmodel/admin/calendar.models.ts @@ -0,0 +1,14 @@ +import { CalendarTypeViewModel } from "./calendarType.models"; + +export interface CalendarViewModel { + id: string; + starttime: Date; + endtime: Date; + title: string; + content: string; + location: string; + allDay: boolean; + createdAt: Date; + updatedAt: Date; + type: CalendarTypeViewModel; +} diff --git a/src/viewmodel/admin/calendarType.models.ts b/src/viewmodel/admin/calendarType.models.ts new file mode 100644 index 0000000..e57dcb1 --- /dev/null +++ b/src/viewmodel/admin/calendarType.models.ts @@ -0,0 +1,6 @@ +export interface CalendarTypeViewModel { + id: number; + type: string; + nscdr: boolean; + color: string; +}