diff --git a/dbal/ts/src/core/entities/user-operations.ts b/dbal/ts/src/core/entities/user-operations.ts new file mode 100644 index 000000000..83c6a0ff8 --- /dev/null +++ b/dbal/ts/src/core/entities/user-operations.ts @@ -0,0 +1,185 @@ +/** + * @file user-operations.ts + * @description User entity CRUD operations for DBAL client + * + * Single-responsibility module following the small-function-file pattern. + */ + +import type { DBALAdapter } from '../../adapters/adapter' +import type { User, ListOptions, ListResult } from '../types' +import { DBALError } from '../errors' +import { validateUserCreate, validateUserUpdate, validateId } from '../validation' + +/** + * Create user operations object for the DBAL client + */ +export const createUserOperations = (adapter: DBALAdapter) => ({ + /** + * Create a new user + */ + create: async (data: Omit): Promise => { + const validationErrors = validateUserCreate(data) + if (validationErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user data', + validationErrors.map(error => ({ field: 'user', error })) + ) + } + + try { + return adapter.create('User', data) as Promise + } catch (error) { + if (error instanceof DBALError && error.code === 409) { + throw DBALError.conflict(`User with username or email already exists`) + } + throw error + } + }, + + /** + * Read a user by ID + */ + read: async (id: string): Promise => { + const validationErrors = validateId(id) + if (validationErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user ID', + validationErrors.map(error => ({ field: 'id', error })) + ) + } + + const result = await adapter.read('User', id) as User | null + if (!result) { + throw DBALError.notFound(`User not found: ${id}`) + } + return result + }, + + /** + * Update an existing user + */ + update: async (id: string, data: Partial): Promise => { + const idErrors = validateId(id) + if (idErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user ID', + idErrors.map(error => ({ field: 'id', error })) + ) + } + + const validationErrors = validateUserUpdate(data) + if (validationErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user update data', + validationErrors.map(error => ({ field: 'user', error })) + ) + } + + try { + return adapter.update('User', id, data) as Promise + } catch (error) { + if (error instanceof DBALError && error.code === 409) { + throw DBALError.conflict(`Username or email already exists`) + } + throw error + } + }, + + /** + * Delete a user by ID + */ + delete: async (id: string): Promise => { + const validationErrors = validateId(id) + if (validationErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user ID', + validationErrors.map(error => ({ field: 'id', error })) + ) + } + + const result = await adapter.delete('User', id) + if (!result) { + throw DBALError.notFound(`User not found: ${id}`) + } + return result + }, + + /** + * List users with filtering and pagination + */ + list: async (options?: ListOptions): Promise> => { + return adapter.list('User', options) as Promise> + }, + + /** + * Batch create multiple users + */ + createMany: async (data: Array>): Promise => { + if (!data || data.length === 0) { + return 0 + } + + const validationErrors = data.flatMap((item, index) => + validateUserCreate(item).map(error => ({ field: `users[${index}]`, error })) + ) + if (validationErrors.length > 0) { + throw DBALError.validationError('Invalid user batch', validationErrors) + } + + try { + return adapter.createMany('User', data as Record[]) + } catch (error) { + if (error instanceof DBALError && error.code === 409) { + throw DBALError.conflict('Username or email already exists') + } + throw error + } + }, + + /** + * Bulk update users matching a filter + */ + updateMany: async (filter: Record, data: Partial): Promise => { + if (!filter || Object.keys(filter).length === 0) { + throw DBALError.validationError('Bulk update requires a filter', [ + { field: 'filter', error: 'Filter is required' }, + ]) + } + + if (!data || Object.keys(data).length === 0) { + throw DBALError.validationError('Bulk update requires data', [ + { field: 'data', error: 'Update data is required' }, + ]) + } + + const validationErrors = validateUserUpdate(data) + if (validationErrors.length > 0) { + throw DBALError.validationError( + 'Invalid user update data', + validationErrors.map(error => ({ field: 'user', error })) + ) + } + + try { + return adapter.updateMany('User', filter, data as Record) + } catch (error) { + if (error instanceof DBALError && error.code === 409) { + throw DBALError.conflict('Username or email already exists') + } + throw error + } + }, + + /** + * Bulk delete users matching a filter + */ + deleteMany: async (filter: Record): Promise => { + if (!filter || Object.keys(filter).length === 0) { + throw DBALError.validationError('Bulk delete requires a filter', [ + { field: 'filter', error: 'Filter is required' }, + ]) + } + + return adapter.deleteMany('User', filter) + }, +}) diff --git a/frontends/nextjs/src/hooks/useAuth.ts b/frontends/nextjs/src/hooks/useAuth.ts index 994473fb0..a5ea83e6d 100644 --- a/frontends/nextjs/src/hooks/useAuth.ts +++ b/frontends/nextjs/src/hooks/useAuth.ts @@ -19,6 +19,10 @@ export function useAuth(): UseAuthReturn { await authStore.login(identifier, password) }, []) + const register = useCallback(async (username: string, email: string, password: string) => { + await authStore.register(username, email, password) + }, []) + const logout = useCallback(async () => { await authStore.logout() }, []) @@ -30,6 +34,7 @@ export function useAuth(): UseAuthReturn { return { ...state, login, + register, logout, refresh, }