diff --git a/README.md b/README.md index b16db57..24521f3 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,11 @@ Create Express TypeScript Starter was created by [Wubshet Zeleke](https://linked ## License Create Express TypeScript Starter is licensed under the MIT License. + + + +## NOTES + +- this appication is an employee facing app/system so i think im going to make the decision to store pins as they are and not hashing them because +there isnt any private data or information that could be used from the app. Creating an employee is simply to have their name in the app in order +to track whos parking which ever cars \ No newline at end of file diff --git a/__tests__/employee.test.ts b/__tests__/employee.test.ts new file mode 100644 index 0000000..79f29e9 --- /dev/null +++ b/__tests__/employee.test.ts @@ -0,0 +1,109 @@ +import request from 'supertest' +import app from '../src/app' +import prisma from '../src/services/prisma' +import { Employee } from '@prisma/client' + +beforeEach(async () => { + // Clear tables with foreign keys first + await prisma.car.deleteMany() // Since Car relates to Employee + await prisma.employee.deleteMany() + await prisma.entrance.deleteMany() + + // Then clear tables without dependencies + await prisma.location.deleteMany() +}) + +test('POST /v1/location/:locationId/employee creates a new employee', async () => { + // First, create a location to associate with the employee + const location = await prisma.location.create({ data: { name: 'Test Location - employee' } }) + + const res = await request(app).post(`/v1/location/${location.id}/employee`).send({ name: 'John Doe', pin: '1234' }) + + expect(res.status).toBe(201) + expect(res.body).toHaveProperty('message', 'Employee created') + expect(res.body.employee).toHaveProperty('name', 'John Doe') + expect(res.body.employee).toHaveProperty('pin', '1234') + expect(res.body.employee).toHaveProperty('locationId', location.id) + + // Verify it exists in the DB + const employeeInDb = await prisma.employee.findFirst({ where: { pin: '1234' } }) + expect(employeeInDb).not.toBeNull() + expect(employeeInDb?.locationId).toBe(location.id) +}) + +test('GET /v1/location/:locationId/employee retrieves employees for a location', async () => { + // Create a location and employees + const location = await prisma.location.create({ data: { name: 'Test Location - get employees' } }) + await prisma.employee.createMany({ + data: [ + { name: 'Alice', pin: '1111', locationId: location.id }, + { name: 'Bob', pin: '2222', locationId: location.id }, + ], + }) + + const res = await request(app).get(`/v1/location/${location.id}/employee`) + + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('message', `Retrieved all employees for location ${location.id}`) + expect(res.body.employees).toHaveLength(2) + const pins = res.body.employees.map((emp: Employee) => emp.pin) + expect(pins).toContain('1111') + expect(pins).toContain('2222') +}) + +test('GET /v1/location/:locationId/employee/:id retrieves a single employee', async () => { + // Create a location and an employee + const location = await prisma.location.create({ data: { name: 'Test Location - get single employee' } }) + const employee = await prisma.employee.create({ + data: { name: 'Charlie', pin: '3333', locationId: location.id }, + }) + + const res = await request(app).get(`/v1/location/${location.id}/employee/${employee.id}`) + + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('message', `Retrieved employee ${employee.id}`) + expect(res.body.employee).toHaveProperty('name', 'Charlie') + expect(res.body.employee).toHaveProperty('pin', '3333') + expect(res.body.employee).toHaveProperty('locationId', location.id) +}) + +test('DELETE /v1/location/:locationId/employee/:id deletes an employee', async () => { + // Create a location and an employee + const location = await prisma.location.create({ data: { name: 'Test Location - delete employee' } }) + const employee = await prisma.employee.create({ + data: { name: 'Dave', pin: '4444', locationId: location.id }, + }) + + const res = await request(app).delete(`/v1/location/${location.id}/employee/${employee.id}`) + + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('message', `Deleted employee ${employee.id}`) + expect(res.body.employee).toHaveProperty('id', employee.id) + + // Verify it's deleted from the DB + const employeeInDb = await prisma.employee.findUnique({ where: { id: employee.id } }) + expect(employeeInDb).toBeNull() +}) + +test('PUT /v1/location/:locationId/employee/:id updates an employee', async () => { + // Create a location and an employee + const location = await prisma.location.create({ data: { name: 'Test Location - update employee' } }) + const employee = await prisma.employee.create({ + data: { name: 'Eve', pin: '5555', locationId: location.id }, + }) + + const res = await request(app) + .put(`/v1/location/${location.id}/employee/${employee.id}`) + .send({ updatedName: 'Eve Updated', pin: '9999' }) + + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('message', `Updated employee ${employee.id}`) + expect(res.body.employee).toHaveProperty('name', 'Eve Updated') + expect(res.body.employee).toHaveProperty('pin', '9999') + + // Verify the updates in the DB + const employeeInDb = await prisma.employee.findUnique({ where: { id: employee.id } }) + expect(employeeInDb).not.toBeNull() + expect(employeeInDb?.name).toBe('Eve Updated') + expect(employeeInDb?.pin).toBe('9999') +}) diff --git a/__tests__/entrance.test.ts b/__tests__/entrance.test.ts index 97969e4..738327e 100644 --- a/__tests__/entrance.test.ts +++ b/__tests__/entrance.test.ts @@ -2,10 +2,10 @@ import request from 'supertest' import app from '../src/app' import prisma from '../src/services/prisma' -beforeEach(async () => { - await prisma.entrance.deleteMany() - await prisma.location.deleteMany() -}) +// beforeEach(async () => { +// await prisma.entrance.deleteMany() +// await prisma.location.deleteMany() +// }) test('POST /v1/location/:locationId/entrance creates a new entrance', async () => { // First, create a location to associate with the entrance diff --git a/prisma/migrations/20250919221914_create_test_table/migration.sql b/prisma/migrations/20250919221914_create_test_table/migration.sql deleted file mode 100644 index 3c74980..0000000 --- a/prisma/migrations/20250919221914_create_test_table/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- CreateTable -CREATE TABLE "public"."Test" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "value" INTEGER NOT NULL, - - CONSTRAINT "Test_pkey" PRIMARY KEY ("id") -); diff --git a/prisma/migrations/20250923223643_migrate_valet_schemas_to_test_db/migration.sql b/prisma/migrations/20250930021027_init/migration.sql similarity index 89% rename from prisma/migrations/20250923223643_migrate_valet_schemas_to_test_db/migration.sql rename to prisma/migrations/20250930021027_init/migration.sql index 324f8f5..9b59e77 100644 --- a/prisma/migrations/20250923223643_migrate_valet_schemas_to_test_db/migration.sql +++ b/prisma/migrations/20250930021027_init/migration.sql @@ -23,6 +23,7 @@ CREATE TABLE "public"."Employee" ( "id" SERIAL NOT NULL, "name" TEXT NOT NULL, "pin" TEXT NOT NULL, + "locationId" INTEGER NOT NULL, CONSTRAINT "Employee_pkey" PRIMARY KEY ("id") ); @@ -56,6 +57,9 @@ CREATE UNIQUE INDEX "Car_ticket_key" ON "public"."Car"("ticket"); -- AddForeignKey ALTER TABLE "public"."Entrance" ADD CONSTRAINT "Entrance_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "public"."Location"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "public"."Employee" ADD CONSTRAINT "Employee_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "public"."Location"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "public"."Car" ADD CONSTRAINT "Car_parkedById_fkey" FOREIGN KEY ("parkedById") REFERENCES "public"."Employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3bac308..8431a44 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,16 +22,13 @@ enum CarStatus { } -model Test { - id Int @id @default(autoincrement()) - name String - value Int -} + model Location { id Int @id @default(autoincrement()) name String entrances Entrance[] + employees Employee[] } model Entrance { @@ -45,6 +42,8 @@ model Employee{ id Int @id @default(autoincrement()) name String pin String @unique + location Location @relation(fields: [locationId], references: [id]) + locationId Int ParkedCars Car[] @relation("ParkedCars") CheckedOutCars Car[] @relation("CheckedOutCars") } diff --git a/src/resources/employee/controller.ts b/src/resources/employee/controller.ts new file mode 100644 index 0000000..5a435b1 --- /dev/null +++ b/src/resources/employee/controller.ts @@ -0,0 +1,107 @@ +import { Request, Response, NextFunction } from 'express' +import { createEmployee, deleteEmployee, getEmployeeById } from '../../services/employeeService' +import prisma from '../../services/prisma' + +const makeEmployee = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, pin } = req.body + const { locationId } = req.params + + if (!name || !pin) { + return res.status(400).json({ error: 'Name and Pin are required' }) + } + + // Check if the PIN already exists + const existingEmployee = await prisma.employee.findUnique({ where: { pin } }) + if (existingEmployee) { + return res.status(409).json({ error: 'An employee with this PIN already exists' }) + } + + const newEmployee = await createEmployee(name, pin, parseInt(locationId)) + + res.status(201).json({ message: 'Employee created', employee: newEmployee }) + } catch (error) { + next(error) + } +} + +const getEmployees = async (req: Request, res: Response, next: NextFunction) => { + try { + const { locationId } = req.params + if (!locationId) { + return res.status(400).json({ error: 'Location ID is required' }) + } + const employees = await prisma.employee.findMany({ where: { locationId: parseInt(locationId) } }) + res.status(200).json({ message: `Retrieved all employees for location ${locationId}`, employees }) + } catch (error) { + next(error) + } +} + +const getSingleEmployee = async (req: Request, res: Response, next: NextFunction) => { + try { + const { employeeId } = req.params + if (!employeeId) { + return res.status(400).json({ error: 'Employee ID is required' }) + } + const employee = await getEmployeeById(parseInt(employeeId)) + if (!employee) { + return res.status(404).json({ error: 'Employee not found' }) + } + + res.status(200).json({ message: `Retrieved employee ${employeeId}`, employee }) + } catch (error) { + next(error) + } +} + +const removeEmployee = async (req: Request, res: Response, next: NextFunction) => { + try { + const { employeeId } = req.params + if (!employeeId) { + return res.status(400).json({ error: 'Employee ID is required' }) + } + const deletedEmployee = await deleteEmployee(parseInt(employeeId)) + if (!deletedEmployee) { + return res.status(404).json({ error: 'Employee not found' }) + } + res.status(200).json({ message: `Deleted employee ${employeeId}`, employee: deletedEmployee }) + } catch (error) { + next(error) + } +} + +const updateEmployee = async (req: Request, res: Response, next: NextFunction) => { + try { + const { employeeId } = req.params + const { updatedName, pin } = req.body + + if (!employeeId) { + return res.status(400).json({ error: 'Employee ID is required' }) + } + + const existingEmployee = await getEmployeeById(parseInt(employeeId)) + if (!existingEmployee) { + return res.status(404).json({ error: 'Employee not found' }) + } + + // If pin is being updated, check for uniqueness + if (pin && pin !== existingEmployee.pin) { + const pinConflict = await prisma.employee.findUnique({ where: { pin } }) + if (pinConflict) { + return res.status(409).json({ error: 'An employee with this PIN already exists' }) + } + } + + const updatedEmployee = await prisma.employee.update({ + where: { id: parseInt(employeeId) }, + data: { name: updatedName || existingEmployee.name, pin: pin || existingEmployee.pin }, + }) + + res.status(200).json({ message: `Updated employee ${employeeId}`, employee: updatedEmployee }) + } catch (error) { + next(error) + } +} + +export default { makeEmployee, getEmployees, getSingleEmployee, removeEmployee, updateEmployee } diff --git a/src/resources/employee/routes.ts b/src/resources/employee/routes.ts new file mode 100644 index 0000000..b35b687 --- /dev/null +++ b/src/resources/employee/routes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express' +import employeeController from './controller' + +const router = Router({ mergeParams: true }) + +// define routes +router.post('/', employeeController.makeEmployee) +router.delete('/:employeeId', employeeController.removeEmployee) +router.get('/', employeeController.getEmployees) +router.get('/:employeeId', employeeController.getSingleEmployee) +router.put('/:employeeId', employeeController.updateEmployee) +export default router diff --git a/src/resources/locations/routes.ts b/src/resources/locations/routes.ts index 5ad36ad..1893b73 100644 --- a/src/resources/locations/routes.ts +++ b/src/resources/locations/routes.ts @@ -1,6 +1,7 @@ import { Router } from 'express' import locationController from './controller' import entranceRouter from '../entrances/routes' +import employeeRouter from '../employee/routes' const router = Router() // define routes @@ -10,5 +11,7 @@ router.get('/', locationController.getLocations) router.get('/:id', locationController.getSingleLocation) router.put('/:id', locationController.updateLocation) +router.use('/:locationId/employee', employeeRouter) router.use('/:locationId/entrance', entranceRouter) + export default router diff --git a/src/services/employeeService.ts b/src/services/employeeService.ts new file mode 100644 index 0000000..9535d4b --- /dev/null +++ b/src/services/employeeService.ts @@ -0,0 +1,46 @@ +// src/services/employeeService.ts +import prisma from './prisma' +import { Employee } from '@prisma/client' + +// Create a new employee for a location +export const createEmployee = async (name: string, pin: string, locationId: number) => { + try { + const employee = await prisma.employee.create({ + data: { name, pin, locationId }, + }) + return employee + } catch (err) { + console.error('Error in createEmployee:', err) + throw err + } +} + +// Get a single employee by ID +export const getEmployeeById = async (id: number): Promise => { + try { + return await prisma.employee.findUnique({ where: { id } }) + } catch (err) { + console.error('Error in getEmployeeById:', err) + throw err + } +} + +// Delete an employee +export const deleteEmployee = async (id: number): Promise => { + try { + return await prisma.employee.delete({ where: { id } }) + } catch (err) { + console.error('Error in deleteEmployee:', err) + throw err + } +} + +// Get all employees for a specific location +export const getEmployeesByLocation = async (locationId: number): Promise => { + try { + return await prisma.employee.findMany({ where: { locationId } }) + } catch (err) { + console.error('Error in getEmployeesByLocation:', err) + throw err + } +}