From e506f0132ee4bde6725f2b4cce800bf686ccfe23 Mon Sep 17 00:00:00 2001 From: Marcus0410 Date: Wed, 26 Feb 2025 09:49:48 +0100 Subject: [PATCH 01/13] implemented email format validation --- src/controllers/user.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..f195183c 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -4,6 +4,11 @@ import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(userToCreate.email)) { + return sendDataResponse(res, 400, { email: 'Invalid email format' }) + } + try { const existingUser = await User.findByEmail(userToCreate.email) From 66c1dac2691111f21190d868c6d0321cbe60e87e Mon Sep 17 00:00:00 2001 From: Marcus0410 Date: Wed, 26 Feb 2025 10:31:01 +0100 Subject: [PATCH 02/13] implemented password validation --- src/controllers/user.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/controllers/user.js b/src/controllers/user.js index f195183c..f261ade0 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -2,13 +2,28 @@ import User from '../domain/user.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { + const rawPassword = req.body.password const userToCreate = await User.fromJson(req.body) + // validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(userToCreate.email)) { return sendDataResponse(res, 400, { email: 'Invalid email format' }) } + // validate password format + // - At least 8 characters in length + // - Contains at least one uppercase letter + // - Contains at least one number + // - Contains at least one special character + const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/ + if (!passwordRegex.test(rawPassword)) { + return sendDataResponse(res, 400, { + password: + 'Password must be at least 8 characters long, contain at least one uppercase letter, one number, and one special character' + }) + } + try { const existingUser = await User.findByEmail(userToCreate.email) From 65ab60eadcff02d3a65d01ba8ac968c15d8c7ac2 Mon Sep 17 00:00:00 2001 From: Thomas Kristiansen Date: Wed, 26 Feb 2025 15:04:32 +0100 Subject: [PATCH 03/13] added new columns to profile table --- .../migration.sql | 6 ++ prisma/schema.prisma | 6 ++ prisma/seed.js | 28 ++++++- src/domain/user.js | 75 ++++++++++++++++++- 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/prisma/migrations/20220407150157_create_user_profile/migration.sql b/prisma/migrations/20220407150157_create_user_profile/migration.sql index ac6a38a8..557cfc7c 100644 --- a/prisma/migrations/20220407150157_create_user_profile/migration.sql +++ b/prisma/migrations/20220407150157_create_user_profile/migration.sql @@ -15,6 +15,12 @@ CREATE TABLE "Profile" ( "lastName" TEXT NOT NULL, "bio" TEXT, "githubUrl" TEXT, + "username" TEXT, + "mobile" TEXT, + "specialism" TEXT, + "startDate" TEXT, + "endDate" TEXT, + "jobTitle" TEXT, CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec5632..b62eb7b6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,12 @@ model Profile { lastName String bio String? githubUrl String? + username String? + mobile String? + startDate String? + endDate String? + specialism String? + jobTitle String? } model Cohort { diff --git a/prisma/seed.js b/prisma/seed.js index 21684795..121139bf 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -12,7 +12,13 @@ async function seed() { 'Joe', 'Bloggs', 'Hello, world!', - 'student1' + 'student1', + 'username', + 'mobile', + 'startDate', + 'endDate', + 'specialism', + 'jobTitle' ) const teacher = await createUser( 'teacher@test.com', @@ -22,6 +28,12 @@ async function seed() { 'Sanchez', 'Hello there!', 'teacher1', + 'username', + 'mobile', + 'startDate', + 'endDate', + 'specialism', + 'jobTitle', 'TEACHER' ) @@ -65,6 +77,12 @@ async function createUser( lastName, bio, githubUrl, + username, + mobile, + startDate, + endDate, + specialism, + jobTitle, role = 'STUDENT' ) { const user = await prisma.user.create({ @@ -78,7 +96,13 @@ async function createUser( firstName, lastName, bio, - githubUrl + githubUrl, + username, + mobile, + startDate, + endDate, + specialism, + jobTitle } } }, diff --git a/src/domain/user.js b/src/domain/user.js index fd7734c7..85adaf11 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -7,7 +7,7 @@ export default class User { * take as inputs, what types they return, and other useful information that JS doesn't have built in * @tutorial https://www.valentinog.com/blog/jsdoc * - * @param { { id: int, cohortId: int, email: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string } } } user + * @param { { id: int, cohortId: int, email: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string, username: string, mobile: string, startDate: string, endDate: string, specialism: string, jobTitle: string } } } user * @returns {User} */ static fromDb(user) { @@ -19,6 +19,12 @@ export default class User { user.email, user.profile?.bio, user.profile?.githubUrl, + user.profile?.username, + user.profile?.mobile, + user.profile?.startDate, + user.profile?.endDate, + user.profile?.specialism, + user.profile?.jobTitle, user.password, user.role ) @@ -26,7 +32,20 @@ export default class User { static async fromJson(json) { // eslint-disable-next-line camelcase - const { firstName, lastName, email, biography, githubUrl, password } = json + const { + firstName, + lastName, + email, + biography, + githubUrl, + username, + mobile, + startDate, + endDate, + specialism, + jobTitle, + password + } = json const passwordHash = await bcrypt.hash(password, 8) @@ -38,6 +57,12 @@ export default class User { email, biography, githubUrl, + username, + mobile, + startDate, + endDate, + specialism, + jobTitle, passwordHash ) } @@ -50,6 +75,12 @@ export default class User { email, bio, githubUrl, + username, + mobile, + startDate = 'January 2025', + endDate = 'June 2025', + specialism = 'Software Developer', + jobTitle, passwordHash = null, role = 'STUDENT' ) { @@ -60,6 +91,12 @@ export default class User { this.email = email this.bio = bio this.githubUrl = githubUrl + this.username = username + this.mobile = mobile + this.startDate = startDate + this.endDate = endDate + this.specialism = specialism + this.jobTitle = jobTitle this.passwordHash = passwordHash this.role = role } @@ -74,7 +111,13 @@ export default class User { lastName: this.lastName, email: this.email, biography: this.bio, - githubUrl: this.githubUrl + githubUrl: this.githubUrl, + username: this.username, + mobile: this.mobile, + startDate: this.startDate, + endDate: this.endDate, + specialism: this.specialism, + jobTitle: this.jobTitle } } } @@ -118,6 +161,32 @@ export default class User { return User.fromDb(createdUser) } + async createProfile(id) { + console.log(typeof id) + const data = { + firstName: this.firstName, + lastName: this.lastName, + githubUrl: this.githubUrl, + bio: this.bio, + username: this.username, + mobile: this.mobile, + startDate: this.startDate, + endDate: this.endDate, + specialism: this.specialism, + jobTitle: this.jobTitle, + user: { + connect: { + id: id + } + } + } + const updatedUser = await dbClient.profile.create({ + data + }) + + return User.fromDb(updatedUser) + } + static async findByEmail(email) { return User._findByUnique('email', email) } From b110fe8dc73d96d448fbd0357add050242e03cd4 Mon Sep 17 00:00:00 2001 From: Magnus-droid Date: Wed, 26 Feb 2025 15:18:25 +0100 Subject: [PATCH 04/13] added profile creation for users --- src/controllers/user.js | 15 +++++++++++++++ src/routes/user.js | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..97e2e3a0 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -65,3 +65,18 @@ export const updateById = async (req, res) => { return sendDataResponse(res, 201, { user: { cohort_id: cohortId } }) } + + +export const createProfile = async (req, res) => { + const paramId = parseInt(req.params.id) + const user = await User.findById(paramId) + + if (user == null) { + return sendDataResponse(res, 404, 'user not found!') + } + + const profile = await User.fromJson(req.body) + const createdProfile = await profile.createProfile(paramId) + + return sendDataResponse(res, 201, createdProfile) +} diff --git a/src/routes/user.js b/src/routes/user.js index 9f63d162..fe3b2253 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,5 +1,5 @@ import { Router } from 'express' -import { create, getById, getAll, updateById } from '../controllers/user.js' +import { create, getById, getAll, updateById, createProfile } from '../controllers/user.js' import { validateAuthentication, validateTeacherRole @@ -11,5 +11,6 @@ router.post('/', create) router.get('/', validateAuthentication, getAll) router.get('/:id', validateAuthentication, getById) router.patch('/:id', validateAuthentication, validateTeacherRole, updateById) +router.post('/profile/:id', validateAuthentication, createProfile) export default router From 4cbbed15b6753a96e5f79d9d4000856748d05fa4 Mon Sep 17 00:00:00 2001 From: Thomas Kristiansen Date: Wed, 26 Feb 2025 15:44:16 +0100 Subject: [PATCH 05/13] Added database migrations --- .../20220407150157_create_user_profile/migration.sql | 6 ------ .../20250226143811_update_user_profile/migration.sql | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20250226143811_update_user_profile/migration.sql diff --git a/prisma/migrations/20220407150157_create_user_profile/migration.sql b/prisma/migrations/20220407150157_create_user_profile/migration.sql index 557cfc7c..ac6a38a8 100644 --- a/prisma/migrations/20220407150157_create_user_profile/migration.sql +++ b/prisma/migrations/20220407150157_create_user_profile/migration.sql @@ -15,12 +15,6 @@ CREATE TABLE "Profile" ( "lastName" TEXT NOT NULL, "bio" TEXT, "githubUrl" TEXT, - "username" TEXT, - "mobile" TEXT, - "specialism" TEXT, - "startDate" TEXT, - "endDate" TEXT, - "jobTitle" TEXT, CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") ); diff --git a/prisma/migrations/20250226143811_update_user_profile/migration.sql b/prisma/migrations/20250226143811_update_user_profile/migration.sql new file mode 100644 index 00000000..c4c143da --- /dev/null +++ b/prisma/migrations/20250226143811_update_user_profile/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "Profile" ADD COLUMN "endDate" TEXT, +ADD COLUMN "jobTitle" TEXT, +ADD COLUMN "mobile" TEXT, +ADD COLUMN "specialism" TEXT, +ADD COLUMN "startDate" TEXT, +ADD COLUMN "username" TEXT; From 1a73c86841ad80df9ca81e2afc8caaf3cee54aa6 Mon Sep 17 00:00:00 2001 From: Magnus-droid Date: Thu, 27 Feb 2025 09:47:27 +0100 Subject: [PATCH 06/13] added openapi spec for profile creation --- docs/openapi.yml | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..f48d3e5f 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -65,6 +65,37 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + + /users/profile/{id}: + post: + tags: + - profile + summary: Create profile + description: Create new profile + operationId: createProfile + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + requestBody: + description: User registration details + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProfile' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedProfile' + /login: post: tags: @@ -368,6 +399,30 @@ components: password: type: string + CreateProfile: + type: object + properties: + firstName: + type: string + lastName: + type: string + bio: + type: string + githubUrl: + type: string + username: + type: string + mobile: + type: string + startDate: + type: string + endDate: + type: string + specialism: + type: string + jobTitle: + type: string + UpdateUser: type: object properties: @@ -457,6 +512,40 @@ components: type: string githubUrl: type: string + + CreatedProfile: + type: object + properties: + status: + type: string + example: success + data: + properties: + user: + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + bio: + type: string + githubUrl: + type: string + username: + type: string + mobile: + type: string + startDate: + type: string + endDate: + type: string + specialism: + type: string + jobTitle: + type: string + login: type: object properties: From 612fe5e014f17db4ab382676ac87b853025bb938 Mon Sep 17 00:00:00 2001 From: Marcus0410 Date: Thu, 27 Feb 2025 10:23:44 +0100 Subject: [PATCH 07/13] Implemented getAll and getById for cohorts --- docs/openapi.yml | 88 ++++++++++++++++++++++++++++++++++++--- src/controllers/cohort.js | 30 ++++++++++++- src/domain/cohort.js | 51 ++++++++++++++++++++++- src/routes/cohort.js | 4 +- 4 files changed, 163 insertions(+), 10 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..d6953bfa 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: Team Dev Server API description: |- - version: 1.0 + version: '1.0' servers: - url: http://localhost:4000/ @@ -183,13 +183,13 @@ paths: content: type: string responses: - 201: + '201': description: success content: application/json: schema: $ref: '#/components/schemas/Post' - 400: + '400': description: fail content: application/json: @@ -244,7 +244,7 @@ paths: content: type: string responses: - 201: + '201': description: success content: application/json: @@ -266,7 +266,64 @@ paths: security: - bearerAuth: [] responses: - 201: + '201': + description: success + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + properties: + cohort: + $ref: '#/components/schemas/Cohort' + '400': + description: fail + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + get: + tags: + - cohort + summary: Get all cohorts + description: This can only be done by a logged in user. + operationId: getAllCohorts + security: + - bearerAuth: [] + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AllUsers' + '500': + description: fail + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /cohorts/{id}: + get: + tags: + - cohort + summary: Get cohort by id + description: This can only be done by a logged in user. + operationId: getCohortById + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 'Id of the cohort to fetch' + required: true + schema: + type: string + responses: + '200': description: success content: application/json: @@ -279,7 +336,13 @@ paths: properties: cohort: $ref: '#/components/schemas/Cohort' - 400: + '404': + description: fail + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': description: fail content: application/json: @@ -307,6 +370,19 @@ components: content: type: string + AllCohorts: + type: object + properties: + status: + type: string + data: + type: object + properties: + cohorts: + type: array + items: + $ref: '#/components/schemas/Cohort' + Cohort: type: object properties: diff --git a/src/controllers/cohort.js b/src/controllers/cohort.js index cc39365b..4e33f775 100644 --- a/src/controllers/cohort.js +++ b/src/controllers/cohort.js @@ -1,4 +1,4 @@ -import { createCohort } from '../domain/cohort.js' +import { Cohort, createCohort } from '../domain/cohort.js' import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { @@ -10,3 +10,31 @@ export const create = async (req, res) => { return sendMessageResponse(res, 500, 'Unable to create cohort') } } + +export const getAll = async (req, res) => { + const foundCohorts = await Cohort.findAll() + + const formattedCohorts = foundCohorts.map((cohort) => { + return { + ...cohort.toJSON().cohort + } + }) + + return sendDataResponse(res, 200, { cohorts: formattedCohorts }) +} + +export const getById = async (req, res) => { + const id = parseInt(req.params.id) + + try { + const foundCohort = await Cohort.findById(id) + + if (!foundCohort) { + return sendDataResponse(res, 404, { id: 'Cohort not found' }) + } + + return sendDataResponse(res, 200, foundCohort) + } catch (e) { + return sendMessageResponse(res, 500, 'Unable to get cohort') + } +} diff --git a/src/domain/cohort.js b/src/domain/cohort.js index abdda73b..185b015e 100644 --- a/src/domain/cohort.js +++ b/src/domain/cohort.js @@ -13,15 +13,62 @@ export async function createCohort() { } export class Cohort { - constructor(id = null) { + constructor(id = null, deliveryLogs = null, users = null) { this.id = id + this.deliveryLogs = deliveryLogs + this.users = users } toJSON() { return { cohort: { - id: this.id + id: this.id, + deliveryLogs: this.deliveryLogs, + users: this.users } } } + + static fromDb(cohort) { + return new Cohort(cohort.id, cohort.deliveryLogs, cohort.users) + } + + static async findAll() { + return Cohort._findMany() + } + + static async findById(id) { + return Cohort._findByUnique('id', id) + } + + static async _findByUnique(key, value) { + const foundCohort = await dbClient.cohort.findUnique({ + where: { + [key]: value + }, + include: { + deliveryLogs: true, + users: true + } + }) + + if (foundCohort) { + return Cohort.fromDb(foundCohort) + } + + return null + } + + static async _findMany(key, value) { + const query = { + include: { + deliveryLogs: true, + users: true + } + } + + const foundCohorts = await dbClient.cohort.findMany(query) + + return foundCohorts.map((cohort) => Cohort.fromDb(cohort)) + } } diff --git a/src/routes/cohort.js b/src/routes/cohort.js index 3cc7813d..ac5d7b63 100644 --- a/src/routes/cohort.js +++ b/src/routes/cohort.js @@ -1,5 +1,5 @@ import { Router } from 'express' -import { create } from '../controllers/cohort.js' +import { create, getAll, getById } from '../controllers/cohort.js' import { validateAuthentication, validateTeacherRole @@ -8,5 +8,7 @@ import { const router = Router() router.post('/', validateAuthentication, validateTeacherRole, create) +router.get('/', validateAuthentication, getAll) +router.get('/:id', validateAuthentication, getById) export default router From d546a7fac36ddb0661d0402487a65326a690e3e5 Mon Sep 17 00:00:00 2001 From: Jonas Finborud Nyman Date: Thu, 27 Feb 2025 11:23:40 +0100 Subject: [PATCH 08/13] search students by name query has been implemented --- src/controllers/user.js | 24 ++++++++++++++++++++++++ src/domain/user.js | 27 +++++++++++++++++++++++++++ src/routes/user.js | 9 ++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index f261ade0..e314ed43 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -55,6 +55,30 @@ export const getById = async (req, res) => { } } +export const getStudentsByName = async (req, res) => { + try { + const { name } = req.params + if (!name) { + return sendMessageResponse(res, 400, 'Search query is required') + } + + const usersByFirstName = await User.findManyThatContainsFirstName(name) + const usersByLastName = await User.findManyThatContainsLastName(name) + + const uniqueUsers = [...usersByFirstName, ...usersByLastName] + .reduce((acc, user) => { + acc.set(user.id, user) // Using a Map to remove duplicates + return acc + }, new Map()) + .values() + + return sendDataResponse(res, 200, [...uniqueUsers]) // Convert back to array + } catch (error) { + console.error('Error fetching users:', error) + return sendMessageResponse(res, 500, 'Unable to get users') + } +} + export const getAll = async (req, res) => { // eslint-disable-next-line camelcase const { first_name: firstName } = req.query diff --git a/src/domain/user.js b/src/domain/user.js index 85adaf11..7df741ac 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -199,6 +199,14 @@ export default class User { return User._findMany('firstName', firstName) } + static async findManyThatContainsFirstName(firstName) { + return User._findManyStudentsContains('firstName', firstName) + } + + static async findManyThatContainsLastName(lastName) { + return User._findManyStudentsContains('lastName', lastName) + } + static async findAll() { return User._findMany() } @@ -239,4 +247,23 @@ export default class User { return foundUsers.map((user) => User.fromDb(user)) } + + static async _findManyStudentsContains(key, value) { + const foundUsers = await dbClient.user.findMany({ + where: { + role: 'STUDENT', + profile: { + [key]: { + contains: value, + mode: 'insensitive' + } + } + }, + include: { + profile: true + } + }) + + return foundUsers.map((user) => User.fromDb(user)) + } } diff --git a/src/routes/user.js b/src/routes/user.js index 9f63d162..b83e59cb 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,5 +1,11 @@ import { Router } from 'express' -import { create, getById, getAll, updateById } from '../controllers/user.js' +import { + create, + getById, + getAll, + updateById, + getStudentsByName +} from '../controllers/user.js' import { validateAuthentication, validateTeacherRole @@ -10,6 +16,7 @@ const router = Router() router.post('/', create) router.get('/', validateAuthentication, getAll) router.get('/:id', validateAuthentication, getById) +router.get('/students/:name', validateAuthentication, getStudentsByName) router.patch('/:id', validateAuthentication, validateTeacherRole, updateById) export default router From 91842e45e54cd739f0e98e3970eb9eaa23558352 Mon Sep 17 00:00:00 2001 From: Jonas Finborud Nyman Date: Thu, 27 Feb 2025 13:27:59 +0100 Subject: [PATCH 09/13] improved endpoint to use search queries in the url, i.e. /users/search?firstName=rick&lastName=sanchez --- src/controllers/user.js | 17 ++++++++++++----- src/domain/user.js | 7 +++---- src/routes/user.js | 4 ++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index e314ed43..95c29baf 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -55,15 +55,22 @@ export const getById = async (req, res) => { } } -export const getStudentsByName = async (req, res) => { +export const getByName = async (req, res) => { try { - const { name } = req.params - if (!name) { + const { firstName, lastName } = req.query + if (!firstName && !lastName) { return sendMessageResponse(res, 400, 'Search query is required') } - const usersByFirstName = await User.findManyThatContainsFirstName(name) - const usersByLastName = await User.findManyThatContainsLastName(name) + let usersByFirstName = [] + let usersByLastName = [] + + if (firstName) { + usersByFirstName = await User.findManyThatContainsFirstName(firstName) + } + if (lastName) { + usersByLastName = await User.findManyThatContainsLastName(lastName) + } const uniqueUsers = [...usersByFirstName, ...usersByLastName] .reduce((acc, user) => { diff --git a/src/domain/user.js b/src/domain/user.js index 7df741ac..81fb5a48 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -200,11 +200,11 @@ export default class User { } static async findManyThatContainsFirstName(firstName) { - return User._findManyStudentsContains('firstName', firstName) + return User._findManyContains('firstName', firstName) } static async findManyThatContainsLastName(lastName) { - return User._findManyStudentsContains('lastName', lastName) + return User._findManyContains('lastName', lastName) } static async findAll() { @@ -248,10 +248,9 @@ export default class User { return foundUsers.map((user) => User.fromDb(user)) } - static async _findManyStudentsContains(key, value) { + static async _findManyContains(key, value) { const foundUsers = await dbClient.user.findMany({ where: { - role: 'STUDENT', profile: { [key]: { contains: value, diff --git a/src/routes/user.js b/src/routes/user.js index b83e59cb..2731b309 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -4,7 +4,7 @@ import { getById, getAll, updateById, - getStudentsByName + getByName } from '../controllers/user.js' import { validateAuthentication, @@ -15,8 +15,8 @@ const router = Router() router.post('/', create) router.get('/', validateAuthentication, getAll) +router.get('/search', validateAuthentication, getByName) router.get('/:id', validateAuthentication, getById) -router.get('/students/:name', validateAuthentication, getStudentsByName) router.patch('/:id', validateAuthentication, validateTeacherRole, updateById) export default router From 026978ba35a56b3df814d14b9baaf109c97b5961 Mon Sep 17 00:00:00 2001 From: Thomas Kristiansen Date: Thu, 27 Feb 2025 13:48:19 +0100 Subject: [PATCH 10/13] refactor the database structure and added the specialism table --- .../migration.sql | 31 ++++++++++ prisma/schema.prisma | 17 ++++-- prisma/seed.js | 44 ++++++------- src/controllers/user.js | 1 - src/domain/cohort.js | 30 ++++++++- src/domain/specialism.js | 61 +++++++++++++++++++ src/domain/user.js | 32 +--------- src/routes/user.js | 8 ++- 8 files changed, 163 insertions(+), 61 deletions(-) create mode 100644 prisma/migrations/20250227124254_update_cohort_added_specialism/migration.sql create mode 100644 src/domain/specialism.js diff --git a/prisma/migrations/20250227124254_update_cohort_added_specialism/migration.sql b/prisma/migrations/20250227124254_update_cohort_added_specialism/migration.sql new file mode 100644 index 00000000..5d970e7f --- /dev/null +++ b/prisma/migrations/20250227124254_update_cohort_added_specialism/migration.sql @@ -0,0 +1,31 @@ +/* + Warnings: + + - You are about to drop the column `endDate` on the `Profile` table. All the data in the column will be lost. + - You are about to drop the column `jobTitle` on the `Profile` table. All the data in the column will be lost. + - You are about to drop the column `specialism` on the `Profile` table. All the data in the column will be lost. + - You are about to drop the column `startDate` on the `Profile` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Cohort" ADD COLUMN "endDate" TEXT, +ADD COLUMN "jobTitle" TEXT, +ADD COLUMN "specialismId" INTEGER, +ADD COLUMN "startDate" TEXT; + +-- AlterTable +ALTER TABLE "Profile" DROP COLUMN "endDate", +DROP COLUMN "jobTitle", +DROP COLUMN "specialism", +DROP COLUMN "startDate"; + +-- CreateTable +CREATE TABLE "Specialism" ( + "id" SERIAL NOT NULL, + "specialismName" TEXT NOT NULL, + + CONSTRAINT "Specialism_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Cohort" ADD CONSTRAINT "Cohort_specialismId_fkey" FOREIGN KEY ("specialismId") REFERENCES "Specialism"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b62eb7b6..6127d7db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,16 +38,23 @@ model Profile { githubUrl String? username String? mobile String? - startDate String? - endDate String? - specialism String? - jobTitle String? } model Cohort { id Int @id @default(autoincrement()) + specialismId Int? + specialism Specialism? @relation(fields: [specialismId], references: [id]) users User[] deliveryLogs DeliveryLog[] + startDate String? + endDate String? + jobTitle String? +} + +model Specialism { + id Int @id @default(autoincrement()) + specialismName String + cohorts Cohort[] } model Post { @@ -72,4 +79,4 @@ model DeliveryLogLine { content String logId Int log DeliveryLog @relation(fields: [logId], references: [id]) -} +} \ No newline at end of file diff --git a/prisma/seed.js b/prisma/seed.js index 121139bf..25c32b23 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -3,7 +3,8 @@ import bcrypt from 'bcrypt' const prisma = new PrismaClient() async function seed() { - const cohort = await createCohort() + const specialism = await createSpecialism('Software Developer') + const cohort = await createCohort(specialism.id, 'January 2025', 'June 2025') const student = await createUser( 'student@test.com', @@ -14,11 +15,7 @@ async function seed() { 'Hello, world!', 'student1', 'username', - 'mobile', - 'startDate', - 'endDate', - 'specialism', - 'jobTitle' + 'mobile' ) const teacher = await createUser( 'teacher@test.com', @@ -30,10 +27,6 @@ async function seed() { 'teacher1', 'username', 'mobile', - 'startDate', - 'endDate', - 'specialism', - 'jobTitle', 'TEACHER' ) @@ -59,9 +52,14 @@ async function createPost(userId, content) { return post } -async function createCohort() { +async function createCohort(specialismId, startDate, endDate, jobTitle) { const cohort = await prisma.cohort.create({ - data: {} + data: { + specialismId, + startDate, + endDate, + jobTitle + } }) console.info('Cohort created', cohort) @@ -69,6 +67,18 @@ async function createCohort() { return cohort } +async function createSpecialism(specialismName) { + const specialism = await prisma.specialism.create({ + data: { + specialismName + } + }) + + console.info('Specialism created', specialism) + + return specialism +} + async function createUser( email, password, @@ -79,10 +89,6 @@ async function createUser( githubUrl, username, mobile, - startDate, - endDate, - specialism, - jobTitle, role = 'STUDENT' ) { const user = await prisma.user.create({ @@ -98,11 +104,7 @@ async function createUser( bio, githubUrl, username, - mobile, - startDate, - endDate, - specialism, - jobTitle + mobile } } }, diff --git a/src/controllers/user.js b/src/controllers/user.js index c91850f2..7230b337 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -86,7 +86,6 @@ export const updateById = async (req, res) => { return sendDataResponse(res, 201, { user: { cohort_id: cohortId } }) } - export const createProfile = async (req, res) => { const paramId = parseInt(req.params.id) const user = await User.findById(paramId) diff --git a/src/domain/cohort.js b/src/domain/cohort.js index 185b015e..1df26ae8 100644 --- a/src/domain/cohort.js +++ b/src/domain/cohort.js @@ -13,10 +13,22 @@ export async function createCohort() { } export class Cohort { - constructor(id = null, deliveryLogs = null, users = null) { + constructor( + id = null, + deliveryLogs = null, + users = null, + startDate, + endDate, + specialism = null, + jobTitle + ) { this.id = id this.deliveryLogs = deliveryLogs this.users = users + this.startDate = startDate + this.endDate = endDate + this.specialism = specialism + this.jobTitle = jobTitle } toJSON() { @@ -24,13 +36,25 @@ export class Cohort { cohort: { id: this.id, deliveryLogs: this.deliveryLogs, - users: this.users + users: this.users, + startDate: this.startDate, + endDate: this.endDate, + specialism: this.specialism, + jobTitle: this.jobTitle } } } static fromDb(cohort) { - return new Cohort(cohort.id, cohort.deliveryLogs, cohort.users) + return new Cohort( + cohort.id, + cohort.deliveryLogs, + cohort.users, + cohort.startDate, + cohort.endDate, + cohort.specialism, + cohort.jobTitle + ) } static async findAll() { diff --git a/src/domain/specialism.js b/src/domain/specialism.js new file mode 100644 index 00000000..c1607898 --- /dev/null +++ b/src/domain/specialism.js @@ -0,0 +1,61 @@ +import dbClient from '../utils/dbClient.js' + +/** + * Create a new specialism in the database + * @returns {Specialism} + */ +export async function createSpecialism() { + const createdSpecialism = await dbClient.specialism.create({ + data: {} + }) + + return new Specialism(createdSpecialism.id) +} + +export class Specialism { + constructor(id = null, specialismName, cohorts = null) { + this.id = id + this.specialismName = specialismName + this.cohorts = cohorts + } + + toJSON() { + return { + specialism: { + id: this.id, + specialismName: this.specialismName, + cohorts: this.cohorts + } + } + } + + static fromDb(specialism) { + return new Specialism( + specialism.id, + specialism.specialismName, + specialism.cohorts + ) + } + + static async findAll() { + return Specialism._findMany() + } + + static async findById(id) { + return Specialism._findByUnique('id', id) + } + + static async _findByUnique(key, value) { + const foundSpecialism = await dbClient.specialism.findUnique({ + where: { + [key]: value + } + }) + + if (foundSpecialism) { + return Specialism.fromDb(foundSpecialism) + } + + return null + } +} diff --git a/src/domain/user.js b/src/domain/user.js index 85adaf11..04173192 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -7,7 +7,7 @@ export default class User { * take as inputs, what types they return, and other useful information that JS doesn't have built in * @tutorial https://www.valentinog.com/blog/jsdoc * - * @param { { id: int, cohortId: int, email: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string, username: string, mobile: string, startDate: string, endDate: string, specialism: string, jobTitle: string } } } user + * @param { { id: int, cohortId: int, email: string, profile: { firstName: string, lastName: string, bio: string, githubUrl: string, username: string, mobile: string } } } user * @returns {User} */ static fromDb(user) { @@ -21,10 +21,6 @@ export default class User { user.profile?.githubUrl, user.profile?.username, user.profile?.mobile, - user.profile?.startDate, - user.profile?.endDate, - user.profile?.specialism, - user.profile?.jobTitle, user.password, user.role ) @@ -40,10 +36,6 @@ export default class User { githubUrl, username, mobile, - startDate, - endDate, - specialism, - jobTitle, password } = json @@ -59,10 +51,6 @@ export default class User { githubUrl, username, mobile, - startDate, - endDate, - specialism, - jobTitle, passwordHash ) } @@ -77,10 +65,6 @@ export default class User { githubUrl, username, mobile, - startDate = 'January 2025', - endDate = 'June 2025', - specialism = 'Software Developer', - jobTitle, passwordHash = null, role = 'STUDENT' ) { @@ -93,10 +77,6 @@ export default class User { this.githubUrl = githubUrl this.username = username this.mobile = mobile - this.startDate = startDate - this.endDate = endDate - this.specialism = specialism - this.jobTitle = jobTitle this.passwordHash = passwordHash this.role = role } @@ -113,11 +93,7 @@ export default class User { biography: this.bio, githubUrl: this.githubUrl, username: this.username, - mobile: this.mobile, - startDate: this.startDate, - endDate: this.endDate, - specialism: this.specialism, - jobTitle: this.jobTitle + mobile: this.mobile } } } @@ -170,10 +146,6 @@ export default class User { bio: this.bio, username: this.username, mobile: this.mobile, - startDate: this.startDate, - endDate: this.endDate, - specialism: this.specialism, - jobTitle: this.jobTitle, user: { connect: { id: id diff --git a/src/routes/user.js b/src/routes/user.js index fe3b2253..acad8e7f 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,5 +1,11 @@ import { Router } from 'express' -import { create, getById, getAll, updateById, createProfile } from '../controllers/user.js' +import { + create, + getById, + getAll, + updateById, + createProfile +} from '../controllers/user.js' import { validateAuthentication, validateTeacherRole From 1c2773e86a8e137fd45efc93435562d994289b46 Mon Sep 17 00:00:00 2001 From: Jonas Finborud Nyman Date: Thu, 27 Feb 2025 14:17:24 +0100 Subject: [PATCH 11/13] openapi.yml file update with endpoint --- docs/openapi.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/openapi.yml b/docs/openapi.yml index 5f2a05f2..de01b9af 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -88,6 +88,46 @@ paths: '400': description: Invalid username/password supplied + + /users/search: + get: + tags: + - user + summary: Search users by first name and/or last name + description: 'Search for users by their first name, last name, or both. Either parameter is optional.' + operationId: searchUsers + security: + - bearerAuth: [] + parameters: + - name: firstName + in: query + description: 'Search users by their first name (optional).' + schema: + type: string + - name: lastName + in: query + description: 'Search users by their last name (optional).' + schema: + type: string + responses: + '200': + description: 'Users found successfully' + content: + application/json: + schema: + $ref: '#/components/schemas/AllUsers' + '400': + description: 'Bad request, invalid query parameters.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 'No users found matching the query.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /users/{id}: get: From 405b9d9eb7c1080977c0fe2870e04625d4dabc89 Mon Sep 17 00:00:00 2001 From: Jonas Finborud Nyman Date: Thu, 27 Feb 2025 14:19:15 +0100 Subject: [PATCH 12/13] openapi.yml file updated with endpoint for search --- src/controllers/user.js | 1 - src/routes/user.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 4b9a86e7..182ed597 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -117,7 +117,6 @@ export const updateById = async (req, res) => { return sendDataResponse(res, 201, { user: { cohort_id: cohortId } }) } - export const createProfile = async (req, res) => { const paramId = parseInt(req.params.id) const user = await User.findById(paramId) diff --git a/src/routes/user.js b/src/routes/user.js index 5444bb7b..de16e9c5 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -4,7 +4,8 @@ import { getById, getAll, updateById, - getByName + getByName, + createProfile } from '../controllers/user.js' import { validateAuthentication, From 3e0652fa601929f92f2c2043a8168f1929342859 Mon Sep 17 00:00:00 2001 From: Jonas Finborud Nyman Date: Fri, 28 Feb 2025 09:30:59 +0100 Subject: [PATCH 13/13] improved search algorithm --- src/controllers/user.js | 25 +++++++++---------------- src/domain/user.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 182ed597..5279a1eb 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -62,24 +62,17 @@ export const getByName = async (req, res) => { return sendMessageResponse(res, 400, 'Search query is required') } - let usersByFirstName = [] - let usersByLastName = [] - - if (firstName) { - usersByFirstName = await User.findManyThatContainsFirstName(firstName) - } - if (lastName) { - usersByLastName = await User.findManyThatContainsLastName(lastName) + let usersList = [] + + if (firstName && lastName) { + usersList = await User.findManyThatContainsBothNames(firstName, lastName) + } else if (firstName) { + usersList = await User.findManyThatContainsFirstName(firstName) + } else if (lastName) { + usersList = await User.findManyThatContainsLastName(lastName) } - const uniqueUsers = [...usersByFirstName, ...usersByLastName] - .reduce((acc, user) => { - acc.set(user.id, user) // Using a Map to remove duplicates - return acc - }, new Map()) - .values() - - return sendDataResponse(res, 200, [...uniqueUsers]) // Convert back to array + return sendDataResponse(res, 200, usersList) } catch (error) { console.error('Error fetching users:', error) return sendMessageResponse(res, 500, 'Unable to get users') diff --git a/src/domain/user.js b/src/domain/user.js index afc3e617..0a991fee 100644 --- a/src/domain/user.js +++ b/src/domain/user.js @@ -179,6 +179,15 @@ export default class User { return User._findManyContains('lastName', lastName) } + static async findManyThatContainsBothNames(firstName, lastName) { + return User._findManyContainsTwoKeys( + 'firstName', + firstName, + 'lastName', + lastName + ) + } + static async findAll() { return User._findMany() } @@ -237,4 +246,33 @@ export default class User { return foundUsers.map((user) => User.fromDb(user)) } + + static async _findManyContainsTwoKeys(key1, value1, key2, value2) { + const whereConditions = { + profile: {} + } + + if (value1) { + whereConditions.profile[key1] = { + contains: value1, + mode: 'insensitive' + } + } + + if (value2) { + whereConditions.profile[key2] = { + contains: value2, + mode: 'insensitive' + } + } + + const foundUsers = await dbClient.user.findMany({ + where: whereConditions, + include: { + profile: true + } + }) + + return foundUsers.map((user) => User.fromDb(user)) + } }