From c910ce89966a92fa736b5cdbd9ff037f5cfc35a1 Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 26 Jan 2026 11:45:30 -0600 Subject: [PATCH] Fix user schema to match database structure Updated backend to use single 'name' field instead of 'first_name' and 'last_name' to match the actual database schema where users table has a 'name' column. Changes: - Updated User and UserPublic interfaces to use 'name' and 'avatar_url' - Updated user validators to use 'name' instead of first/last names - Updated all SQL queries in user.service.ts to select u.name - Updated search filters and sort columns - Fixed user creation and update operations This resolves the "column u.first_name does not exist" error. --- water-api/src/controllers/user.controller.ts | 4 +- water-api/src/routes/user.routes.ts | 4 +- water-api/src/services/user.service.ts | 48 ++++++++++---------- water-api/src/types/index.ts | 8 ++-- water-api/src/validators/user.validator.ts | 34 +++++++------- 5 files changed, 50 insertions(+), 48 deletions(-) diff --git a/water-api/src/controllers/user.controller.ts b/water-api/src/controllers/user.controller.ts index 4c9da56..0e10953 100644 --- a/water-api/src/controllers/user.controller.ts +++ b/water-api/src/controllers/user.controller.ts @@ -128,8 +128,8 @@ export async function createUser( const user = await userService.create({ email: data.email, password: data.password, - first_name: data.first_name, - last_name: data.last_name, + name: data.name, + avatar_url: data.avatar_url, role_id: data.role_id, is_active: data.is_active, }); diff --git a/water-api/src/routes/user.routes.ts b/water-api/src/routes/user.routes.ts index 4bcbe6b..d5f480b 100644 --- a/water-api/src/routes/user.routes.ts +++ b/water-api/src/routes/user.routes.ts @@ -32,7 +32,7 @@ router.get('/:id', userController.getUserById); /** * POST /users * Create a new user (admin only) - * Body: { email, password, first_name, last_name, role_id, is_active? } + * Body: { email, password, name, avatar_url?, role_id, is_active? } * Response: { success, message, data: User } */ router.post('/', requireRole('ADMIN'), validateCreateUser, userController.createUser); @@ -40,7 +40,7 @@ router.post('/', requireRole('ADMIN'), validateCreateUser, userController.create /** * PUT /users/:id * Update a user (admin can update all, self can update limited fields) - * Body: { email?, first_name?, last_name?, role_id?, is_active? } + * Body: { email?, name?, avatar_url?, role_id?, is_active? } * Response: { success, message, data: User } */ router.put('/:id', validateUpdateUser, userController.updateUser); diff --git a/water-api/src/services/user.service.ts b/water-api/src/services/user.service.ts index 59c6faf..2c4c8cd 100644 --- a/water-api/src/services/user.service.ts +++ b/water-api/src/services/user.service.ts @@ -61,7 +61,7 @@ export async function getAll( if (filters?.search) { conditions.push( - `(u.first_name ILIKE $${paramIndex} OR u.last_name ILIKE $${paramIndex} OR u.email ILIKE $${paramIndex})` + `(u.name ILIKE $${paramIndex} OR u.email ILIKE $${paramIndex})` ); params.push(`%${filters.search}%`); paramIndex++; @@ -70,7 +70,7 @@ export async function getAll( const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Validate sortBy to prevent SQL injection - const allowedSortColumns = ['created_at', 'updated_at', 'email', 'first_name', 'last_name']; + const allowedSortColumns = ['created_at', 'updated_at', 'email', 'name']; const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at'; const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC'; @@ -88,8 +88,8 @@ export async function getAll( SELECT u.id, u.email, - u.first_name, - u.last_name, + u.name, + u.avatar_url, u.role_id, r.name as role_name, r.description as role_description, @@ -109,8 +109,8 @@ export async function getAll( const users: UserPublic[] = usersResult.rows.map((row) => ({ id: row.id, email: row.email, - first_name: row.first_name, - last_name: row.last_name, + name: row.name, + avatar_url: row.avatar_url, role_id: row.role_id, role: row.role_name ? { @@ -154,8 +154,8 @@ export async function getById(id: number): Promise { SELECT u.id, u.email, - u.first_name, - u.last_name, + u.name, + u.avatar_url, u.role_id, r.name as role_name, r.description as role_description, @@ -179,8 +179,8 @@ export async function getById(id: number): Promise { return { id: row.id, email: row.email, - first_name: row.first_name, - last_name: row.last_name, + name: row.name, + avatar_url: row.avatar_url, role_id: row.role_id, role: row.role_name ? { @@ -232,8 +232,8 @@ export async function getByEmail(email: string): Promise { export async function create(data: { email: string; password: string; - first_name: string; - last_name: string; + name: string; + avatar_url?: string | null; role_id: number; is_active?: boolean; }): Promise { @@ -248,15 +248,15 @@ export async function create(data: { const result = await query( ` - INSERT INTO users (email, password_hash, first_name, last_name, role_id, is_active) + INSERT INTO users (email, password_hash, name, avatar_url, role_id, is_active) VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, email, first_name, last_name, role_id, is_active, last_login, created_at, updated_at + RETURNING id, email, name, avatar_url, role_id, is_active, last_login, created_at, updated_at `, [ data.email.toLowerCase(), password_hash, - data.first_name, - data.last_name, + data.name, + data.avatar_url ?? null, data.role_id, data.is_active ?? true, ] @@ -278,8 +278,8 @@ export async function update( id: number, data: { email?: string; - first_name?: string; - last_name?: string; + name?: string; + avatar_url?: string | null; role_id?: number; is_active?: boolean; } @@ -309,15 +309,15 @@ export async function update( paramIndex++; } - if (data.first_name !== undefined) { - updates.push(`first_name = $${paramIndex}`); - params.push(data.first_name); + if (data.name !== undefined) { + updates.push(`name = $${paramIndex}`); + params.push(data.name); paramIndex++; } - if (data.last_name !== undefined) { - updates.push(`last_name = $${paramIndex}`); - params.push(data.last_name); + if (data.avatar_url !== undefined) { + updates.push(`avatar_url = $${paramIndex}`); + params.push(data.avatar_url); paramIndex++; } diff --git a/water-api/src/types/index.ts b/water-api/src/types/index.ts index 775cd3d..9c34d33 100644 --- a/water-api/src/types/index.ts +++ b/water-api/src/types/index.ts @@ -26,8 +26,8 @@ export interface User { id: number; email: string; password_hash: string; - first_name: string; - last_name: string; + name: string; + avatar_url: string | null; role_id: number; role?: Role; is_active: boolean; @@ -39,8 +39,8 @@ export interface User { export interface UserPublic { id: number; email: string; - first_name: string; - last_name: string; + name: string; + avatar_url: string | null; role_id: number; role?: Role; is_active: boolean; diff --git a/water-api/src/validators/user.validator.ts b/water-api/src/validators/user.validator.ts index 850a29a..da7c95d 100644 --- a/water-api/src/validators/user.validator.ts +++ b/water-api/src/validators/user.validator.ts @@ -5,7 +5,8 @@ import { Request, Response, NextFunction } from 'express'; * Schema for creating a new user * - email: required, must be valid email format * - password: required, minimum 8 characters - * - name: required (first_name + last_name combined or separate) + * - name: required, full name + * - avatar_url: optional, URL to avatar image * - role_id: required, must be valid UUID * - is_active: optional, defaults to true */ @@ -17,14 +18,15 @@ export const createUserSchema = z.object({ password: z .string({ required_error: 'Password is required' }) .min(8, 'Password must be at least 8 characters'), - first_name: z - .string({ required_error: 'First name is required' }) - .min(1, 'First name cannot be empty') - .max(100, 'First name cannot exceed 100 characters'), - last_name: z - .string({ required_error: 'Last name is required' }) - .min(1, 'Last name cannot be empty') - .max(100, 'Last name cannot exceed 100 characters'), + name: z + .string({ required_error: 'Name is required' }) + .min(1, 'Name cannot be empty') + .max(255, 'Name cannot exceed 255 characters'), + avatar_url: z + .string() + .url('Avatar URL must be a valid URL') + .optional() + .nullable(), role_id: z .number({ required_error: 'Role ID is required' }) .int('Role ID must be an integer') @@ -43,16 +45,16 @@ export const updateUserSchema = z.object({ .email('Invalid email format') .transform((val) => val.toLowerCase().trim()) .optional(), - first_name: z + name: z .string() - .min(1, 'First name cannot be empty') - .max(100, 'First name cannot exceed 100 characters') + .min(1, 'Name cannot be empty') + .max(255, 'Name cannot exceed 255 characters') .optional(), - last_name: z + avatar_url: z .string() - .min(1, 'Last name cannot be empty') - .max(100, 'Last name cannot exceed 100 characters') - .optional(), + .url('Avatar URL must be a valid URL') + .optional() + .nullable(), role_id: z .number() .int('Role ID must be an integer')