From 5f313df75453d1b91740b132b85481047ed43895 Mon Sep 17 00:00:00 2001 From: Roi Cohen Date: Mon, 20 Oct 2025 17:48:32 +0300 Subject: [PATCH 1/4] feat: Initial import of Version 2 content from the disconnected feature/v2 branch --- .gitignore | 22 ++++ ormconfig.ts | 7 ++ package-lock.json | 100 +++++++++++++++++- package.json | 18 +++- src/app.ts | 11 +- src/config/config.ts | 46 ++++++++ src/config/database.ts | 6 ++ src/constants/httpStatus.ts | 16 +++ src/controllers/userController.ts | 62 +++++++++++ src/entity/User.ts | 11 +- src/middleware/requestLogger.ts | 17 +++ src/middleware/validateRequest.ts | 15 +++ .../1700000000000-CreateUsersTable.ts | 66 ++++++++++++ .../1760834879732-RemoveUserTimestamps.ts | 33 ++++++ src/routes/userRoutes.ts | 17 +++ src/server.ts | 10 +- src/services/userService.ts | 70 ++++++++++++ src/validators/emailValidator.ts | 17 +++ src/validators/userSchema.ts | 16 +++ tsconfig.json | 0 20 files changed, 535 insertions(+), 25 deletions(-) mode change 100644 => 100755 .gitignore create mode 100755 ormconfig.ts mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json mode change 100644 => 100755 src/app.ts create mode 100755 src/config/config.ts create mode 100755 src/config/database.ts create mode 100755 src/constants/httpStatus.ts create mode 100755 src/controllers/userController.ts mode change 100644 => 100755 src/entity/User.ts create mode 100644 src/middleware/requestLogger.ts create mode 100644 src/middleware/validateRequest.ts create mode 100755 src/migrations/1700000000000-CreateUsersTable.ts create mode 100644 src/migrations/1760834879732-RemoveUserTimestamps.ts create mode 100755 src/routes/userRoutes.ts mode change 100644 => 100755 src/server.ts create mode 100755 src/services/userService.ts create mode 100644 src/validators/emailValidator.ts create mode 100644 src/validators/userSchema.ts mode change 100644 => 100755 tsconfig.json diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index deed335..9c50be9 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,25 @@ node_modules/ dist/ +build/ .env +.env.local +.env.*.local +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +.vscode/ +.idea/ +*.swp +*.swo +*~ +coverage/ +.nyc_output/ +*.tmp +*.temp +project_code.txt diff --git a/ormconfig.ts b/ormconfig.ts new file mode 100755 index 0000000..11a9fa8 --- /dev/null +++ b/ormconfig.ts @@ -0,0 +1,7 @@ +import { DataSource } from 'typeorm'; +import config from './src/config/config'; + +export default new DataSource({ + ...config.typeorm, + migrations: ['src/migrations/*.ts'], +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index d0657b1..2b894f0 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,23 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@map-colonies/error-types": "*", + "config": "^3.3.12", "dotenv": "^16.4.5", "express": "^4.19.2", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "zod": "^3.25.76" }, "devDependencies": { + "@types/config": "^3.3.5", "@types/express": "^4.17.21", - "@types/node": "^20.11.30", + "@types/node": "^20.19.22", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" @@ -133,6 +139,26 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, + "node_modules/@map-colonies/error-express-handler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@map-colonies/error-express-handler/-/error-express-handler-2.1.0.tgz", + "integrity": "sha512-8qcyePq5JVrbEw7rioZ7nQfYavVw8OiFGwfAJolDmq045ppm82IEKBFMgzLC4p4dbRj+wDzwcuRkcv5yGE0IZA==", + "license": "ISC", + "dependencies": { + "http-status-codes": "^2.1.4" + } + }, + "node_modules/@map-colonies/error-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@map-colonies/error-types/-/error-types-1.3.1.tgz", + "integrity": "sha512-ZcXiCYcjk4SBhAxO6JGJZ9cmiCInBULpisrnTViPsdxtfk+1a6XG/sKXop5U5se6xQZ77L43ZEUhiwvE7FsaPA==", + "license": "ISC", + "dependencies": { + "@map-colonies/error-express-handler": "^2.0.0", + "express": "^4.17.1", + "http-status-codes": "^2.1.4" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -195,6 +221,13 @@ "@types/node": "*" } }, + "node_modules/@types/config": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.5.tgz", + "integrity": "sha512-itq2HtXQBrNUKwMNZnb9mBRE3T99VYCdl1gjST9rq+9kFaB1iMMGuDeZnP88qid73DnpAMKH9ZolqDpS1Lz7+w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -252,9 +285,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.15.tgz", - "integrity": "sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==", + "version": "20.19.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz", + "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -312,6 +345,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -779,6 +830,18 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/config": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", + "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1401,6 +1464,12 @@ "node": ">= 0.8" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1596,6 +1665,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -3320,6 +3401,15 @@ "engines": { "node": "^12.20.0 || >=14" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 05f4f02..5a13795 --- a/package.json +++ b/package.json @@ -6,22 +6,32 @@ "scripts": { "dev": "ts-node-dev --respawn src/server.ts", "build": "tsc", - "start": "node dist/server.js" + "start": "node dist/server.js", + "typeorm": "typeorm-ts-node-commonjs -d ormconfig.ts", + "migration:generate": "npm run typeorm migration:generate", + "migration:run": "npm run typeorm migration:run", + "migration:revert": "npm run typeorm migration:revert" }, "dependencies": { + "@map-colonies/error-types": "*", + "config": "^3.3.12", "dotenv": "^16.4.5", "express": "^4.19.2", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "zod": "^3.25.76" }, "devDependencies": { + "@types/config": "^3.3.5", "@types/express": "^4.17.21", - "@types/node": "^20.11.30", + "@types/node": "^20.19.22", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } -} +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts old mode 100644 new mode 100755 index 506355f..8bd1e27 --- a/src/app.ts +++ b/src/app.ts @@ -1,14 +1,13 @@ import express from 'express'; -import usersRouter from './routes/users'; -import { errorHandler } from './middleware/errorHandler'; - +import userRoutes from './routes/userRoutes'; +import { getErrorHandlerMiddleware } from '@map-colonies/error-express-handler'; const app = express(); -app.use(express.json()); +app.use(express.json()); -app.use('/users', usersRouter); +app.use('/users', userRoutes); -app.use(errorHandler); +app.use(getErrorHandlerMiddleware()); export default app; diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100755 index 0000000..3013926 --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,46 @@ +import dotenv from 'dotenv'; +import { DataSourceOptions } from 'typeorm'; +import { User } from '../entity/User'; + +dotenv.config(); + +interface Config { + port: number; + nodeEnv: string; + database: { + host: string; + port: number; + name: string; + user: string; + password: string; + }; + typeorm: DataSourceOptions; +} + +const config: Config = { + port: parseInt(process.env.PORT || '3000', 10), + nodeEnv: process.env.NODE_ENV || 'development', + database: { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + name: process.env.DB_NAME || 'myapp', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || '', + }, + typeorm: { + type: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'myapp', + synchronize: false, + logging: process.env.NODE_ENV === 'development', + entities: [User], + migrations: ['src/migrations/*.ts'], + migrationsTableName: 'migrations', + migrationsRun: false, + }, +}; + +export default config; \ No newline at end of file diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100755 index 0000000..f77aa5e --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,6 @@ +import { DataSource } from 'typeorm'; +import config from './config'; + +export const AppDataSource = new DataSource(config.typeorm); + +export default AppDataSource; \ No newline at end of file diff --git a/src/constants/httpStatus.ts b/src/constants/httpStatus.ts new file mode 100755 index 0000000..7100634 --- /dev/null +++ b/src/constants/httpStatus.ts @@ -0,0 +1,16 @@ +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + NO_CONTENT: 204, + + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +} as const; + +export type HttpStatusCode = typeof HTTP_STATUS[keyof typeof HTTP_STATUS]; \ No newline at end of file diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts new file mode 100755 index 0000000..0aad124 --- /dev/null +++ b/src/controllers/userController.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from 'express'; +import { userService } from '../services/userService'; +import { HTTP_STATUS } from '../constants/httpStatus'; + +export const userController = { + getAllUsers: async (req: Request, res: Response, next: NextFunction) => { + try { + const users = await userService.getAll(); + res.status(HTTP_STATUS.OK).json(users); + } catch (e) { + next(e); + } + }, + + getUserById: async (req: Request, res: Response, next: NextFunction) => { + try { + const user = await userService.getById(parseInt(req.params.id)); + res.status(HTTP_STATUS.OK).json(user); + } catch (e) { + next(e); + } + }, + + getUsersByHobby: async (req: Request, res: Response, next: NextFunction) => { + try { + const { hobbyName } = req.params; + const users = await userService.getByHobby(hobbyName); + res.status(HTTP_STATUS.OK).json(users); + } catch (e) { + next(e); + } + }, + + createUser: async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, name, hobbies } = req.body; + const saved = await userService.create(email, name, hobbies || []); + res.status(HTTP_STATUS.CREATED).json(saved); + } catch (e) { + next(e); + } + }, + + updateUser: async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, name, hobbies } = req.body; + const updated = await userService.update(parseInt(req.params.id), email, name, hobbies); + res.status(HTTP_STATUS.OK).json(updated); + } catch (e) { + next(e); + } + }, + + deleteUser: async (req: Request, res: Response, next: NextFunction) => { + try { + await userService.delete(parseInt(req.params.id)); + res.status(HTTP_STATUS.NO_CONTENT).send(); + } catch (e) { + next(e); + } + } +}; \ No newline at end of file diff --git a/src/entity/User.ts b/src/entity/User.ts old mode 100644 new mode 100755 index a47535f..2830eea --- a/src/entity/User.ts +++ b/src/entity/User.ts @@ -1,13 +1,16 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; -@Entity() +@Entity('users') export class User { @PrimaryGeneratedColumn() id!: number; @Column() + name!: string; + + @Column({ unique: true }) email!: string; - @Column() - name!: string; + @Column('simple-array', { default: '' }) + hobbies!: string[]; } \ No newline at end of file diff --git a/src/middleware/requestLogger.ts b/src/middleware/requestLogger.ts new file mode 100644 index 0000000..18ab812 --- /dev/null +++ b/src/middleware/requestLogger.ts @@ -0,0 +1,17 @@ +import { Request, Response, NextFunction } from 'express'; + +export const requestLogger = (req: Request, res: Response, next: NextFunction) => { + const timestamp = new Date().toISOString(); + const method = req.method; + const url = req.originalUrl; + const body = req.body; + + console.log('--- Request Log ---'); + console.log(`Timestamp: ${timestamp}`); + console.log(`Method: ${method}`); + console.log(`URL: ${url}`); + console.log(`Body:`, body); + console.log('------------------'); + + next(); +}; \ No newline at end of file diff --git a/src/middleware/validateRequest.ts b/src/middleware/validateRequest.ts new file mode 100644 index 0000000..9f3039b --- /dev/null +++ b/src/middleware/validateRequest.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express'; +import { ZodSchema } from 'zod'; +import { BadRequestError } from '@map-colonies/error-types'; + +export const validateRequest = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse(req.body); + next(); + } catch (error: any) { + const errorMessage = error.errors?.map((e: any) => e.message).join(', ') || 'Validation failed'; + next(new BadRequestError(errorMessage)); + } + }; +}; \ No newline at end of file diff --git a/src/migrations/1700000000000-CreateUsersTable.ts b/src/migrations/1700000000000-CreateUsersTable.ts new file mode 100755 index 0000000..f07a5ed --- /dev/null +++ b/src/migrations/1700000000000-CreateUsersTable.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateUsersTable1700000000000 implements MigrationInterface { + name = 'CreateUsersTable1700000000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'users', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'name', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'email', + type: 'varchar', + length: '255', + isNullable: false, + isUnique: true, + }, + { + name: 'hobbies', + type: 'text', + isNullable: true, + default: "''", + }, + { + name: 'created_at', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP', + isNullable: false, + }, + { + name: 'updated_at', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP', + onUpdate: 'CURRENT_TIMESTAMP', + isNullable: false, + }, + ], + indices: [ + { + name: 'IDX_USER_EMAIL', + columnNames: ['email'], + isUnique: true, + }, + ], + }), + true, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('users'); + } +} \ No newline at end of file diff --git a/src/migrations/1760834879732-RemoveUserTimestamps.ts b/src/migrations/1760834879732-RemoveUserTimestamps.ts new file mode 100644 index 0000000..95c521c --- /dev/null +++ b/src/migrations/1760834879732-RemoveUserTimestamps.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class RemoveUserTimestamps1700000000001 implements MigrationInterface { + name = 'RemoveUserTimestamps1700000000001' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("users", "created_at"); + await queryRunner.dropColumn("users", "updated_at"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + "users", + new TableColumn({ + name: "created_at", + type: "timestamp", + default: "CURRENT_TIMESTAMP", + isNullable: false, + }) + ); + + await queryRunner.addColumn( + "users", + new TableColumn({ + name: "updated_at", + type: "timestamp", + default: "CURRENT_TIMESTAMP", + onUpdate: "CURRENT_TIMESTAMP", + isNullable: false, + }) + ); + } +} \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts new file mode 100755 index 0000000..11310d5 --- /dev/null +++ b/src/routes/userRoutes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { userController } from '../controllers/userController'; +import { requestLogger } from '../middleware/requestLogger'; +import { validateRequest } from '../middleware/validateRequest'; +import { createUserSchema, updateUserSchema } from '../validators/userSchema'; + +const router = Router(); + +router.get('/', userController.getAllUsers); +router.get('/hobby/:hobbyName', userController.getUsersByHobby); +router.get('/:id', userController.getUserById); + +router.post('/', requestLogger, validateRequest(createUserSchema), userController.createUser); +router.put('/:id', requestLogger, validateRequest(updateUserSchema), userController.updateUser); +router.delete('/:id', requestLogger, userController.deleteUser); + +export default router; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts old mode 100644 new mode 100755 index 42cc8eb..b22e546 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,12 @@ -import 'reflect-metadata'; -import { AppDataSource } from './data-source'; +import config from './config/config'; +import { AppDataSource } from './config/database'; import app from './app'; -const PORT = process.env.PORT || 3000; - const startServer = async () => { try { await AppDataSource.initialize(); - app.listen(PORT, () => { - console.log(`API running at http://localhost:${PORT}`); + app.listen(config.port, () => { + console.log(`API running at http://localhost:${config.port}`); }); } catch (err) { console.error('Error initializing DB:', err); diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100755 index 0000000..66be965 --- /dev/null +++ b/src/services/userService.ts @@ -0,0 +1,70 @@ +import { AppDataSource } from '../config/database'; +import { User } from '../entity/User'; +import { NotFoundError, ConflictError, BadRequestError } from '@map-colonies/error-types'; +import { EmailValidator } from '../validators/emailValidator'; + +const userRepository = AppDataSource.getRepository(User); + +export const userService = { + getAll: async (): Promise => { + return await userRepository.find(); + }, + + getById: async (id: number): Promise => { + const user = await userRepository.findOne({ where: { id } }); + if (!user) { + throw new NotFoundError(`User with id ${id} not found`); + } + return user; + }, + + getByHobby: async (hobbyName: string): Promise => { + return await userRepository + .createQueryBuilder('user') + .where('user.hobbies LIKE :hobby', { hobby: `%${hobbyName}%` }) + .getMany(); + }, + + create: async (email: string, name: string, hobbies: string[]): Promise => { + try { + EmailValidator.validate(email); + } catch (error) { + throw new BadRequestError((error as Error).message); + } + + const existing = await userRepository.findOne({ where: { email } }); + if (existing) { + throw new ConflictError(`User with email ${email} already exists`); + } + + const user = userRepository.create({ email, name, hobbies }); + return await userRepository.save(user); + }, + + update: async (id: number, email: string, name: string, hobbies?: string[]): Promise => { + try { + EmailValidator.validate(email); + } catch (error) { + throw new BadRequestError((error as Error).message); + } + + const user = await userRepository.findOne({ where: { id } }); + if (!user) { + throw new NotFoundError(`User with id ${id} not found`); + } + + user.email = email; + user.name = name; + if (hobbies !== undefined) { + user.hobbies = hobbies; + } + return await userRepository.save(user); + }, + + delete: async (id: number): Promise => { + const result = await userRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundError(`User with id ${id} not found`); + } + } +}; \ No newline at end of file diff --git a/src/validators/emailValidator.ts b/src/validators/emailValidator.ts new file mode 100644 index 0000000..456384a --- /dev/null +++ b/src/validators/emailValidator.ts @@ -0,0 +1,17 @@ +export class EmailValidator { + private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + static isValid(email: string): boolean { + return this.EMAIL_REGEX.test(email); + } + + static validate(email: string): void { + if (!email || email.trim() === '') { + throw new Error('Email is required'); + } + + if (!this.isValid(email)) { + throw new Error('Invalid email format'); + } + } +} \ No newline at end of file diff --git a/src/validators/userSchema.ts b/src/validators/userSchema.ts new file mode 100644 index 0000000..137a604 --- /dev/null +++ b/src/validators/userSchema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const createUserSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100, 'Name too long'), + email: z.string().email('Invalid email format'), + hobbies: z.array(z.string()).optional().default([]) +}); + +export const updateUserSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100, 'Name too long'), + email: z.string().email('Invalid email format'), + hobbies: z.array(z.string()).optional() +}); + +export type CreateUserInput = z.infer; +export type UpdateUserInput = z.infer; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755 From a1b812fd7b86eab5503bbe680ba9782bac863a45 Mon Sep 17 00:00:00 2001 From: roicohen Date: Wed, 22 Oct 2025 10:32:30 +0300 Subject: [PATCH 2/4] Remove data-source.ts --- src/data-source.ts | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/data-source.ts diff --git a/src/data-source.ts b/src/data-source.ts deleted file mode 100644 index 849b076..0000000 --- a/src/data-source.ts +++ /dev/null @@ -1,18 +0,0 @@ -import 'reflect-metadata'; -import { DataSource } from 'typeorm'; -import { config } from 'dotenv'; -import { User } from './entity/User'; - -config(); - -export const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT || 5432), - username: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - entities: [User], - synchronize: true, - logging: true -}); \ No newline at end of file From 2e7eb0f31707402a49e0a77b1a1466e15befbbce Mon Sep 17 00:00:00 2001 From: roicohen Date: Wed, 22 Oct 2025 18:00:26 +0300 Subject: [PATCH 3/4] remove un necessary files --- .gitignore | 3 +- ormconfig.ts | 2 +- src/constants/HttpStatusCode.ts | 14 ++++++++ src/controllers/userController.ts | 14 ++++---- src/controllers/users.ts | 57 ------------------------------- src/middleware/errorHandler.ts | 10 ------ src/routes/users.ts | 12 ------- src/services/users.ts | 44 ------------------------ 8 files changed, 23 insertions(+), 133 deletions(-) create mode 100755 src/constants/HttpStatusCode.ts delete mode 100644 src/controllers/users.ts delete mode 100644 src/middleware/errorHandler.ts delete mode 100644 src/routes/users.ts delete mode 100644 src/services/users.ts diff --git a/.gitignore b/.gitignore index 9c50be9..fa87605 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ node_modules/ dist/ build/ -.env -.env.local +.env* .env.*.local *.log npm-debug.log* diff --git a/ormconfig.ts b/ormconfig.ts index 11a9fa8..74523de 100755 --- a/ormconfig.ts +++ b/ormconfig.ts @@ -4,4 +4,4 @@ import config from './src/config/config'; export default new DataSource({ ...config.typeorm, migrations: ['src/migrations/*.ts'], -}); \ No newline at end of file +}); diff --git a/src/constants/HttpStatusCode.ts b/src/constants/HttpStatusCode.ts new file mode 100755 index 0000000..048dc2f --- /dev/null +++ b/src/constants/HttpStatusCode.ts @@ -0,0 +1,14 @@ +export { + OK, + CREATED, + NO_CONTENT, + BAD_REQUEST, + UNAUTHORIZED, + FORBIDDEN, + NOT_FOUND, + CONFLICT, + INTERNAL_SERVER_ERROR, + SERVICE_UNAVAILABLE, + type StatusCodes as HttpStatusCode +} from 'http-status-codes'; + diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 0aad124..defc375 100755 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -1,12 +1,12 @@ import { Request, Response, NextFunction } from 'express'; import { userService } from '../services/userService'; -import { HTTP_STATUS } from '../constants/httpStatus'; +import { OK, CREATED, NO_CONTENT } from '../constants/httpStatus'; export const userController = { getAllUsers: async (req: Request, res: Response, next: NextFunction) => { try { const users = await userService.getAll(); - res.status(HTTP_STATUS.OK).json(users); + res.status(OK).json(users); } catch (e) { next(e); } @@ -15,7 +15,7 @@ export const userController = { getUserById: async (req: Request, res: Response, next: NextFunction) => { try { const user = await userService.getById(parseInt(req.params.id)); - res.status(HTTP_STATUS.OK).json(user); + res.status(OK).json(user); } catch (e) { next(e); } @@ -25,7 +25,7 @@ export const userController = { try { const { hobbyName } = req.params; const users = await userService.getByHobby(hobbyName); - res.status(HTTP_STATUS.OK).json(users); + res.status(OK).json(users); } catch (e) { next(e); } @@ -35,7 +35,7 @@ export const userController = { try { const { email, name, hobbies } = req.body; const saved = await userService.create(email, name, hobbies || []); - res.status(HTTP_STATUS.CREATED).json(saved); + res.status(CREATED).json(saved); } catch (e) { next(e); } @@ -45,7 +45,7 @@ export const userController = { try { const { email, name, hobbies } = req.body; const updated = await userService.update(parseInt(req.params.id), email, name, hobbies); - res.status(HTTP_STATUS.OK).json(updated); + res.status(OK).json(updated); } catch (e) { next(e); } @@ -54,7 +54,7 @@ export const userController = { deleteUser: async (req: Request, res: Response, next: NextFunction) => { try { await userService.delete(parseInt(req.params.id)); - res.status(HTTP_STATUS.NO_CONTENT).send(); + res.status(NO_CONTENT).send(); } catch (e) { next(e); } diff --git a/src/controllers/users.ts b/src/controllers/users.ts deleted file mode 100644 index 05d5555..0000000 --- a/src/controllers/users.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { userService } from '../services/users'; - -export const userController = { - getAllUsers: async (req: Request, res: Response, next: NextFunction) => { - try { - const users = await userService.getAll(); - res.status(200).json(users); - } catch (e) { - next(e); - } - }, - - getUserById: async (req: Request, res: Response, next: NextFunction) => { - try { - const user = await userService.getById(parseInt(req.params.id)); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - res.status(200).json(user); - } catch (e) { - next(e); - } - }, - - createUser: async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, name } = req.body; - if (!email || !name) { - return res.status(400).json({ error: 'email and name are required' }); - } - const saved = await userService.create(email, name); - res.status(201).json(saved); - } catch (e) { - next(e); - } - }, - - updateUser: async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, name } = req.body; - const updated = await userService.update(parseInt(req.params.id), email, name); - res.status(200).json(updated); - } catch (e) { - next(e); - } - }, - - deleteUser: async (req: Request, res: Response, next: NextFunction) => { - try { - await userService.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (e) { - next(e); - } - } -}; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts deleted file mode 100644 index 0733261..0000000 --- a/src/middleware/errorHandler.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { - console.error('Unhandled Error:', err); - - const status = err.status || 500; - const message = err.message || 'Internal Server Error'; - - res.status(status).json({ error: message }); -}; diff --git a/src/routes/users.ts b/src/routes/users.ts deleted file mode 100644 index c3f10f4..0000000 --- a/src/routes/users.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from 'express'; -import { userController } from '../controllers/users'; - -const router = Router(); - -router.get('/', userController.getAllUsers); -router.get('/:id', userController.getUserById); -router.post('/', userController.createUser); -router.put('/:id', userController.updateUser); -router.delete('/:id', userController.deleteUser); - -export default router; \ No newline at end of file diff --git a/src/services/users.ts b/src/services/users.ts deleted file mode 100644 index f26db8e..0000000 --- a/src/services/users.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AppDataSource } from '../data-source'; -import { User } from '../entity/User'; - -const repo = () => AppDataSource.getRepository(User); - -export const userService = { - getAll: async () => { - return await repo().find(); - }, - - getById: async (id: number) => { - return await repo().findOneBy({ id }); - }, - - create: async (email: string, name: string) => { - const exists = await repo().findOneBy({ email }); - if (exists) { - throw { status: 409, message: 'email already exists' }; - } - - const user = repo().create({ email, name }); - return await repo().save(user); - }, - - update: async (id: number, email?: string, name?: string) => { - const user = await repo().findOneBy({ id }); - if (!user) { - throw { status: 404, message: 'User not found' }; - } - - if (email) user.email = email; - if (name) user.name = name; - - return await repo().save(user); - }, - - delete: async (id: number) => { - const result = await repo().delete(id); - if (result.affected === 0) { - throw { status: 404, message: 'User not found' }; - } - return result; - } -}; From fc738939e46d401340b216546ba298466c9153c9 Mon Sep 17 00:00:00 2001 From: roicohen Date: Wed, 22 Oct 2025 18:08:34 +0300 Subject: [PATCH 4/4] deleted httpstatus.ts --- package-lock.json | 3 +-- package.json | 4 ++-- src/constants/httpStatus.ts | 16 ---------------- 3 files changed, 3 insertions(+), 20 deletions(-) delete mode 100755 src/constants/httpStatus.ts diff --git a/package-lock.json b/package-lock.json index 2b894f0..e151d71 100755 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@map-colonies/error-types": "*", + "@map-colonies/error-types": "^1.3.1", "config": "^3.3.12", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -152,7 +152,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/@map-colonies/error-types/-/error-types-1.3.1.tgz", "integrity": "sha512-ZcXiCYcjk4SBhAxO6JGJZ9cmiCInBULpisrnTViPsdxtfk+1a6XG/sKXop5U5se6xQZ77L43ZEUhiwvE7FsaPA==", - "license": "ISC", "dependencies": { "@map-colonies/error-express-handler": "^2.0.0", "express": "^4.17.1", diff --git a/package.json b/package.json index 5a13795..e5b3d51 100755 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "migration:revert": "npm run typeorm migration:revert" }, "dependencies": { - "@map-colonies/error-types": "*", + "@map-colonies/error-types": "^1.3.1", "config": "^3.3.12", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -34,4 +34,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } -} \ No newline at end of file +} diff --git a/src/constants/httpStatus.ts b/src/constants/httpStatus.ts deleted file mode 100755 index 7100634..0000000 --- a/src/constants/httpStatus.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const HTTP_STATUS = { - OK: 200, - CREATED: 201, - NO_CONTENT: 204, - - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - CONFLICT: 409, - - INTERNAL_SERVER_ERROR: 500, - SERVICE_UNAVAILABLE: 503, -} as const; - -export type HttpStatusCode = typeof HTTP_STATUS[keyof typeof HTTP_STATUS]; \ No newline at end of file