diff --git a/src/controllers/flags.ts b/src/controllers/flags.ts index ae49ab1..dc820ef 100644 --- a/src/controllers/flags.ts +++ b/src/controllers/flags.ts @@ -1,4 +1,4 @@ -import { getAllFlags } from '../models/flags'; +import { getAllFlags, checkDuplicateKey, createFlag } from '../models/flags'; import { Request, Response } from 'express'; import { IResponse } from '../types/response'; import { IFlag } from '../types/flags'; @@ -24,3 +24,36 @@ export const getFlagsController = async ( } } }; + +export const createFlagController = async ( + req: Request, + res: Response, +): Promise => { + const flag: IFlag = req.body; + try { + const isDuplicate = await checkDuplicateKey(flag.key, flag.project_id); + if (isDuplicate) { + const response: IResponse = { + status: 400, + message: 'Flag key already exists', + data: flag, + }; + res.status(400).json(response); + return; + } + + const createdFlag = await createFlag(flag); + const response: IResponse = { + status: 201, + message: 'Flag created successfully', + data: createdFlag, + }; + res.status(201).json(response); + } catch (error: unknown) { + if (error instanceof Error) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'An unknown error occurred' }); + } + } +}; diff --git a/src/middlewares/flags.ts b/src/middlewares/flags.ts new file mode 100644 index 0000000..bf224f6 --- /dev/null +++ b/src/middlewares/flags.ts @@ -0,0 +1,119 @@ +import Joi from 'joi'; +import { NextFunction, Request, Response } from 'express'; + +const errorMessages = { + name: { + 'string.empty': 'Flag name cannot be empty', + 'any.required': 'Flag name is required', + }, + description: { + 'string.empty': 'Description cannot be empty if provided', + }, + key: { + 'string.empty': 'Flag key cannot be empty', + 'any.required': 'Flag key is required', + }, + project_id: { + 'string.empty': 'Project ID cannot be empty', + 'any.required': 'Project ID is required', + }, + environment_id: { + 'string.empty': 'Environment ID cannot be empty', + 'any.required': 'Environment ID is required', + }, + is_active: { + 'boolean.base': 'Is active must be a boolean value', + 'any.required': 'Is active status is required', + }, + expires_at: { + 'date.base': 'Expiration date must be a valid date', + 'date.greater': 'Expiration date must be in the future', + }, + added_by: { + 'string.empty': 'Added by cannot be empty', + 'any.required': 'Added by is required', + }, +}; + +export const createFlagValidator = Joi.object({ + name: Joi.string() + .trim() + .min(3) + .max(100) + .required() + .messages(errorMessages.name), + + description: Joi.string() + .trim() + .max(500) + .allow('') + .optional() + .messages(errorMessages.description), + + key: Joi.string() + .trim() + .pattern(/^[a-zA-Z0-9_-]+$/) + .min(3) + .max(50) + .required() + .messages({ + ...errorMessages.key, + 'string.pattern.base': + 'Flag key must contain only letters, numbers, hyphens and underscores', + }), + + project_id: Joi.string() + .trim() + .uuid() + .required() + .messages({ + ...errorMessages.project_id, + 'string.guid': 'Project ID must be a valid UUID', + }), + + environment_id: Joi.string() + .trim() + .uuid() + .required() + .messages({ + ...errorMessages.environment_id, + 'string.guid': 'Environment ID must be a valid UUID', + }), + + is_active: Joi.boolean().required().messages(errorMessages.is_active), + + expires_at: Joi.date() + .greater('now') + .allow(null) + .optional() + .messages({ + ...errorMessages.expires_at, + 'date.greater': 'Expiration date must be in the future', + }), + + added_by: Joi.string() + .trim() + .uuid() + .required() + .messages({ + ...errorMessages.added_by, + 'string.guid': 'Added by must be a valid UUID', + }), +}); + +export const validateCreateFlag = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + await createFlagValidator.validateAsync(req.body); + next(); + } catch (error: unknown) { + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + } else { + res.status(400).json({ error: 'An unknown error occurred' }); + } + } +}; diff --git a/src/models/flags.ts b/src/models/flags.ts index 1761eee..090dd9b 100644 --- a/src/models/flags.ts +++ b/src/models/flags.ts @@ -19,3 +19,44 @@ export const getAllFlags = async (): Promise => { added_by: flag.added_by, })); }; + +export const createFlag = async (flag: IFlag): Promise => { + const createdFlag = await prisma.flag.create({ + data: { + name: flag.name, + description: flag.description, + key: flag.key, + project_id: flag.project_id, + environment_id: flag.environment_id, + is_active: flag.is_active, + expires_at: flag.expires_at, + added_by: flag.added_by, + }, + }); + return { + id: createdFlag.id, + name: createdFlag.name, + description: createdFlag.description, + key: createdFlag.key, + project_id: createdFlag.project_id, + environment_id: createdFlag.environment_id, + is_active: createdFlag.is_active, + expires_at: createdFlag.expires_at, + created_at: createdFlag.created_at.toISOString(), + updated_at: createdFlag.updated_at.toISOString(), + added_by: createdFlag.added_by, + }; +}; + +export const checkDuplicateKey = async ( + key: string, + project_id: string, +): Promise => { + const flag = await prisma.flag.findFirst({ + where: { + key, + project_id, + }, + }); + return flag !== null; +}; diff --git a/src/routes/flags.ts b/src/routes/flags.ts index 91d90ff..c6e5b3f 100644 --- a/src/routes/flags.ts +++ b/src/routes/flags.ts @@ -1,6 +1,8 @@ -import { getFlagsController } from '../controllers/flags'; +import { getFlagsController, createFlagController } from '../controllers/flags'; import express from 'express'; +import { validateCreateFlag } from '../middlewares/flags'; export default (router: express.Router) => { router.get('/flags', getFlagsController); + router.post('/flags', validateCreateFlag, createFlagController); };