updated last web service

This commit is contained in:
Berkay 2025-06-02 21:11:15 +03:00
parent df3f59bd8e
commit 0cd0eb0f22
106 changed files with 1061 additions and 50 deletions

View File

@ -0,0 +1,30 @@
FROM python:3.12-slim
WORKDIR /
# Install system dependencies and Poetry
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir poetry
# Copy Poetry configuration
COPY /pyproject.toml ./pyproject.toml
# Configure Poetry and install dependencies with optimizations
RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi --no-root --only main && pip cache purge && rm -rf ~/.cache/pypoetry
# Copy application code
COPY /ServicesApi/Initializer /Initializer
COPY /ServicesApi/Controllers /Controllers
COPY /ServicesApi/Validations /Validations
COPY /ServicesApi/Schemas /Schemas
COPY /ServicesApi/Extensions /Extensions
COPY /ServicesApi/Builds/TestApi/endpoints /endpoints
COPY /ServicesApi/Builds/TestApi/events /events
# COPY /api_services/api_builds/test_api/validations /api_initializer/validations
# COPY /api_services/api_builds/test_api/index.py /api_initializer/index.py
# Set Python path to include app directory
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
# Run the application using the configured uvicorn server
CMD ["poetry", "run", "python", "/Initializer/app.py"]

View File

@ -0,0 +1,18 @@
from fastapi import APIRouter
from .tester.router import tester_endpoint_route
def get_routes() -> list[APIRouter]:
return [tester_endpoint_route]
def get_safe_endpoint_urls() -> list[tuple[str, str]]:
return [
("/", "GET"),
("/docs", "GET"),
("/redoc", "GET"),
("/openapi.json", "GET"),
("/metrics", "GET"),
("/tester/list", "POST"),
]

View File

@ -0,0 +1,37 @@
import datetime
from typing import Any
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from Validations.response import PaginateOnly, Pagination, PaginationResult, EndpointResponse
from Validations.defaults.validations import CommonHeaders
from Schemas import AccountRecords
tester_endpoint_route = APIRouter(prefix="/tester", tags=["Tester Cluster"])
class TestList(BaseModel):
uu_id: str
bank_date: datetime.datetime
currency_value: float
process_name: str
tester_list = "TestList"
@tester_endpoint_route.post(
path="/list",
description="List all tester endpoint",
operation_id="4c38fab8-9b66-41cd-b87a-41175c9eea48",
)
def tester_list_route(
list_options: PaginateOnly,
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
):
with AccountRecords.new_session() as db_session:
AccountRecords.set_session(db_session)
tester_list = AccountRecords.query.filter(AccountRecords.currency_value > 0)
pagination = Pagination(data=tester_list, base_query=AccountRecords.query.filter())
pagination.change(**list_options.model_dump())
pagination_result = PaginationResult(data=tester_list, pagination=pagination, response_model=TestList)
return EndpointResponse(message="MSG0003-LIST", pagination_result=pagination_result).response

View File

@ -0,0 +1,3 @@
__all__ = []

View File

@ -1,11 +1,12 @@
from contextlib import contextmanager
from functools import lru_cache
from typing import Generator
from api_controllers.postgres.config import postgres_configs
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
from Controllers.Postgres.config import postgres_configs
# Configure the database engine with proper pooling
engine = create_engine(

View File

@ -13,7 +13,7 @@ from sqlalchemy_mixins.repr import ReprMixin
from sqlalchemy_mixins.smartquery import SmartQueryMixin
from sqlalchemy_mixins.activerecord import ActiveRecordMixin
from api_controllers.postgres.engine import get_db, Base
from Controllers.Postgres.engine import get_db, Base
T = TypeVar("CrudMixin", bound="CrudMixin")
@ -72,7 +72,7 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
return True, str(arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss"))
elif isinstance(val, bool):
return True, bool(val)
elif isinstance(val, (float, Decimal)):
elif isinstance(val, float) or isinstance(val, Decimal):
return True, round(float(val), 3)
elif isinstance(val, int):
return True, int(val)
@ -81,7 +81,6 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
elif val is None:
return True, None
return False, None
except Exception as e:
err = e
return False, None
@ -160,7 +159,6 @@ class BasicMixin(Base, ActiveRecordMixin, SerializeMixin, ReprMixin, SmartQueryM
return {}
class CrudMixin(BasicMixin):
"""
Base mixin providing CRUD operations and common fields for PostgreSQL models.
@ -272,4 +270,3 @@ class CrudCollection(CrudMixin):
is_email_send: Mapped[bool] = mapped_column(
Boolean, server_default="0", comment="Email sent flag"
)

View File

@ -0,0 +1,19 @@
from fastapi import Request, status
from fastapi.responses import JSONResponse
from config import api_config
from endpoints.routes import get_safe_endpoint_urls
async def token_middleware(request: Request, call_next):
base_url = request.url.path
safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()]
if base_url in safe_endpoints:
return await call_next(request)
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
if not token:
return JSONResponse(content={"error": "EYS_0002"}, status_code=status.HTTP_401_UNAUTHORIZED)
response = await call_next(request)
return response

View File

@ -0,0 +1,96 @@
import enum
from typing import Optional, Union, Dict, Any, List
from pydantic import BaseModel
from api_controllers.redis.database import RedisActions
from api_validations.token.validations import (
TokenDictType,
OccupantTokenObject,
EmployeeTokenObject,
UserType,
)
class TokenProvider:
AUTH_TOKEN: str = "AUTH_TOKEN"
@classmethod
def convert_redis_object_to_token(cls, redis_object: Dict[str, Any]) -> TokenDictType:
"""
Process Redis object and return appropriate token object.
"""
if redis_object.get("user_type") == UserType.employee.value:
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == UserType.occupant.value:
return OccupantTokenObject(**redis_object)
raise ValueError("Invalid user type")
@classmethod
def get_login_token_from_redis(
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None
) -> Union[TokenDictType, List[TokenDictType]]:
"""
Retrieve token object from Redis using token and user_uu_id
"""
token_to_use, user_uu_id_to_use = token or "*", user_uu_id or "*"
list_of_token_dict, auth_key_list = [], [cls.AUTH_TOKEN, token_to_use, user_uu_id_to_use]
if token:
result = RedisActions.get_json(list_keys=auth_key_list, limit=1)
if first_record := result.first:
return cls.convert_redis_object_to_token(first_record)
elif user_uu_id:
result = RedisActions.get_json(list_keys=auth_key_list)
if all_records := result.all:
for all_record in all_records:
list_of_token_dict.append(cls.convert_redis_object_to_token(all_record))
return list_of_token_dict
raise ValueError("Token not found in Redis. Please check the token or user_uu_id.")
@classmethod
def get_dict_from_redis(
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None
) -> Union[TokenDictType, List[TokenDictType]]:
"""
Retrieve token object from Redis using token and user_uu_id
"""
token_to_use, user_uu_id_to_use = token or "*", user_uu_id or "*"
list_of_token_dict, auth_key_list = [], [cls.AUTH_TOKEN, token_to_use, user_uu_id_to_use, "*"]
if token:
result = RedisActions.get_json(list_keys=auth_key_list, limit=1)
if first_record := result.first:
return cls.convert_redis_object_to_token(first_record)
elif user_uu_id:
result = RedisActions.get_json(list_keys=auth_key_list)
if all_records := result.all:
for all_record in all_records:
list_of_token_dict.append(cls.convert_redis_object_to_token(all_record))
return list_of_token_dict
raise ValueError("Token not found in Redis. Please check the token or user_uu_id.")
@classmethod
def retrieve_application_codes(cls, page_url: str, token: TokenDictType):
"""
Retrieve application code from the token object or list of token objects.
"""
if isinstance(token, EmployeeTokenObject):
if application_codes := token.selected_company.reachable_app_codes.get(page_url, None):
return application_codes
elif isinstance(token, OccupantTokenObject):
if application_codes := token.selected_occupant.reachable_app_codes.get(page_url, None):
return application_codes
raise ValueError("Invalid token type or no application code found.")
@classmethod
def retrieve_event_codes(cls, endpoint_code: str, token: TokenDictType) -> str:
"""
Retrieve event code from the token object or list of token objects.
"""
if isinstance(token, EmployeeTokenObject):
if event_codes := token.selected_company.reachable_event_codes.get(endpoint_code, None):
return event_codes
elif isinstance(token, OccupantTokenObject):
if event_codes := token.selected_occupant.reachable_event_codes.get(endpoint_code, None):
return event_codes
raise ValueError("Invalid token type or no event code found.")

View File

@ -1,7 +1,7 @@
import uvicorn
from api_initializer.config import api_config
from api_initializer.create_app import create_app
from config import api_config
from create_app import create_app
# from prometheus_fastapi_instrumentator import Instrumentator

View File

@ -1,3 +1,5 @@
import events
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
@ -6,9 +8,8 @@ from config import api_config
from open_api_creator import create_openapi_schema
from create_route import RouteRegisterController
from api_middlewares.token_middleware import token_middleware
from Extensions.Middlewares.token_middleware import token_middleware
from endpoints.routes import get_routes
import events
cluster_is_set = False

View File

@ -10,7 +10,7 @@ class RouteRegisterController:
@staticmethod
def add_router_with_event_to_database(router: APIRouter):
from schemas import EndpointRestriction
from Schemas import EndpointRestriction
with EndpointRestriction.new_session() as db_session:
EndpointRestriction.set_session(db_session)

View File

@ -1,4 +1,4 @@
from ApiControllers.postgres.mixin import CrudCollection
from Controllers.Postgres.mixin import CrudCollection
from sqlalchemy.orm import mapped_column, Mapped, relationship
from sqlalchemy import (
String,

View File

@ -31,8 +31,8 @@ class EndpointResponse(BaseModel):
return {
"completed": self.completed,
"message": self.message,
"data": result_data,
"pagination": pagination_dict,
"data": result_data,
}
model_config = {

View File

@ -26,7 +26,8 @@ class Pagination:
MIN_SIZE = default_paginate_config.MIN_SIZE
MAX_SIZE = default_paginate_config.MAX_SIZE
def __init__(self, data: PostgresResponse):
def __init__(self, data: Query, base_query: Query):
self.base_query = base_query
self.query = data
self.size: int = self.DEFAULT_SIZE
self.page: int = 1
@ -60,7 +61,7 @@ class Pagination:
"""Update page counts and validate current page."""
if self.query:
self.total_count = self.query.count()
self.all_count = self.query.count()
self.all_count = self.base_query.count()
self.size = (
self.size
@ -151,24 +152,14 @@ class PaginationResult:
Ordered query object.
"""
if not len(self.order_by) == len(self.pagination.orderType):
raise ValueError(
"Order by fields and order types must have the same length."
)
raise ValueError("Order by fields and order types must have the same length.")
order_criteria = zip(self.order_by, self.pagination.orderType)
for field, direction in order_criteria:
if hasattr(self._query.column_descriptions[0]["entity"], field):
if direction.lower().startswith("d"):
self._query = self._query.order_by(
desc(
getattr(self._query.column_descriptions[0]["entity"], field)
)
)
self._query = self._query.order_by(desc(getattr(self._query.column_descriptions[0]["entity"], field)))
else:
self._query = self._query.order_by(
asc(
getattr(self._query.column_descriptions[0]["entity"], field)
)
)
self._query = self._query.order_by(asc(getattr(self._query.column_descriptions[0]["entity"], field)))
return self._query
@property

View File

View File

@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { fetchTest } from "@/fetchers/custom/test/fetch";
export async function GET() {
try {
return NextResponse.json({ status: 200, data: { message: "Test" } });
} catch (error) {
return NextResponse.json({ status: 500, message: "No data is found" });
}
}
export async function POST(request: Request) {
const body = await request.json();
try {
const data = await fetchTest({
page: body.page,
size: body.size,
orderField: body.orderField,
orderType: body.orderType,
query: body.query,
});
return NextResponse.json({ status: 200, data });
} catch (error) {
return NextResponse.json({ status: 500, message: "No data is found" });
}
}

View File

@ -0,0 +1,169 @@
'use client';
import React, { useState } from "react";
import { apiPostFetcher, apiGetFetcher } from "@/lib/fetcher"
import { API_BASE_URL } from "@/config/config"
import { Button } from "@/components/mutual/ui/button"
import { Input } from "@/components/mutual/ui/input";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
// Define the form schema with Zod
const formSchema = z.object({
page: z.number().min(1, "Page must be at least 1"),
size: z.number().min(1, "Size must be at least 1"),
orderField: z.array(z.string()),
orderType: z.array(z.string()),
query: z.any()
});
// Define the type for our form values
type FormValues = z.infer<typeof formSchema>;
export default function TestPage() {
const [testPostResult, setTestPostResult] = useState({});
const [testGetResult, setTestGetResult] = useState({});
// Initialize the form
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
page: 1,
size: 10,
orderField: [],
orderType: [],
query: {}
}
});
const testPost = async (values: FormValues) => {
const result = await apiPostFetcher({
url: `${API_BASE_URL}/test`,
body: values,
isNoCache: true
})
setTestPostResult(result);
}
const testGet = async () => {
const result = await apiGetFetcher({
url: `${API_BASE_URL}/test`,
isNoCache: true
})
setTestGetResult(result);
}
return (
<div className="container flex flex-col gap-4 mx-auto my-10">
<Form {...form}>
<form onSubmit={form.handleSubmit(testPost)} className="flex flex-col gap-4">
<FormField
control={form.control}
name="page"
render={({ field }) => (
<FormItem>
<FormLabel>Page</FormLabel>
<FormControl>
<Input type="number" placeholder="Page" {...field} onChange={e => field.onChange(Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="size"
render={({ field }) => (
<FormItem>
<FormLabel>Size</FormLabel>
<FormControl>
<Input type="number" placeholder="Size" {...field} onChange={e => field.onChange(Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="orderField"
render={({ field }) => (
<FormItem>
<FormLabel>Order Field</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Order Field"
value={field.value.join(",")}
onChange={e => field.onChange(e.target.value.split(","))}
/>
</FormControl>
<FormDescription>Comma-separated list of fields</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="orderType"
render={({ field }) => (
<FormItem>
<FormLabel>Order Type</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Order Type"
value={field.value.join(",")}
onChange={e => field.onChange(e.target.value.split(","))}
/>
</FormControl>
<FormDescription>Comma-separated list of types (asc/desc)</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="query"
render={({ field }) => (
<FormItem>
<FormLabel>Query</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Query"
value={JSON.stringify(field.value)}
onChange={e => {
try {
field.onChange(JSON.parse(e.target.value))
} catch (error) {
// Handle JSON parse error
}
}}
/>
</FormControl>
<FormDescription>JSON format</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Test Post</Button>
</form>
</Form>
<Button onClick={testGet}>Test Get</Button>
{/* <div>{JSON.stringify(testPostResult?.data?.data || [])}</div> */}
<div className="grid grid-cols-4 gap-4">
{(testPostResult?.data?.data?.map((item: any) => (
<div className="flex flex-col gap-2" key={item.uu_id}>
<span key={`${item.uu_id}-uu_id`} >UUID:{item.uu_id}</span>
<span key={`${item.uu_id}-name`}>Name:{item.process_name}</span>
<span key={`${item.uu_id}-bank_date`}>Bank Date:{item.bank_date}</span>
<span key={`${item.uu_id}-currency_value`}>Currency Value:{item.currency_value}</span>
</div>
))
)}
</div>
</div>
);
}

View File

@ -4,7 +4,7 @@ import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/mutual/ui/button"
function AlertDialog({
...props

View File

@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/mutual/ui/button"
function Calendar({
className,

View File

@ -11,7 +11,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
} from "@/components/mutual/ui/dialog"
function Command({
className,

View File

@ -14,7 +14,7 @@ import {
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Label } from "@/components/mutual/ui/label"
const Form = FormProvider

View File

@ -0,0 +1,29 @@
"use server";
import { fetchData } from "@/fetchers/fecther";
import { urlTesterList } from "@/fetchers/index";
export const fetchTest = async ({
page,
size,
orderField,
orderType,
query,
}: {
page?: number;
size?: number;
orderField?: string[];
orderType?: string[];
query?: any;
}) => {
try {
const response = await fetchData(urlTesterList, { method: "POST", cache: false, payload: { page, size, orderField, orderType, query } });
return response.data;
} catch (error) {
console.error(error);
return {
completed: false,
data: null,
message: "No data is found",
};
}
};

View File

@ -1,36 +1,36 @@
import { formatServiceUrl } from "./utils";
const baseUrlAuth = formatServiceUrl(
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "localhost:8001"
);
const baseUrlRestriction = formatServiceUrl(
process.env.NEXT_PUBLIC_RESTRICTION_SERVICE_URL || "restriction_service:8002"
process.env.NEXT_PUBLIC_RESTRICTION_SERVICE_URL || "localhost:8002"
);
const baseUrlApplication = formatServiceUrl(
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "management_service:8003"
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "localhost:8003"
);
const baseUrlAccount = formatServiceUrl(
process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || "account_service:8004"
process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || "localhost:8004"
);
const baseUrlBuilding = formatServiceUrl(
process.env.NEXT_PUBLIC_BUILDING_SERVICE_URL || "building_service:8006"
process.env.NEXT_PUBLIC_BUILDING_SERVICE_URL || "localhost:8006"
);
const baseUrlPeople = formatServiceUrl(
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "validation_service:8009"
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "localhost:8009"
);
const baseUrlTester = formatServiceUrl(
process.env.NEXT_PUBLIC_TESTER_SERVICE_URL || "localhost:8005"
);
const urlCheckToken = `${baseUrlAuth}/authentication/token/check`;
const urlPageValid = `${baseUrlRestriction}/restrictions/page/valid`;
const urlSiteUrls = `${baseUrlRestriction}/restrictions/sites/list`;
const urlTesterList = `${baseUrlTester}/tester/list`;
export {
baseUrlAuth,
baseUrlPeople,
baseUrlApplication,
baseUrlAccount,
baseUrlBuilding,
baseUrlRestriction,
urlCheckToken,
urlPageValid,
urlSiteUrls,
// For test use only
urlTesterList,
};

View File

@ -2,7 +2,7 @@ import { cookies } from "next/headers";
import { fetchDataWithToken } from "@/fetchers/fecther";
import { urlCheckToken, urlPageValid, urlSiteUrls } from "@/fetchers/index";
import { nextCrypto } from "@/fetchers/base";
import { AuthError } from "@/validations/mutual/context/validations";
import { AuthError } from "@/fetchers/types/context";
import { fetchResponseStatus } from "@/fetchers/utils";
async function checkAccessTokenIsValid() {

View File

@ -0,0 +1,165 @@
import { NextRequest } from "next/server";
import {
successResponse,
errorResponse,
paginationResponse,
createResponse,
updateResponse,
deleteResponse,
} from "./responseHandlers";
import { withErrorHandling, validateRequiredFields } from "./requestHandlers";
import {
ApiHandler,
PaginationParams,
ListFunction,
CreateFunction,
UpdateFunction,
DeleteFunction,
} from "./types";
/**
* Generic list operation handler
* @param request NextRequest object
* @param body Request body
* @param listFunction The function to call to get the list data
*/
export async function handleListOperation(
request: NextRequest,
body: any,
listFunction: ListFunction
) {
const page = body.page || 1;
const size = body.size || 10;
const orderField = body.orderField || ["uu_id"];
const orderType = body.orderType || ["asc"];
const query = body.query || {};
const response = await listFunction({
page,
size,
orderField,
orderType,
query,
} as PaginationParams);
return paginationResponse(response.data, response.pagination);
}
/**
* Generic create operation handler
* @param request NextRequest object
* @param body Request body
* @param createFunction The function to call to create the item
* @param requiredFields Array of required field names
*/
export async function handleCreateOperation(
body: any,
createFunction?: CreateFunction,
requiredFields: string[] = []
) {
if (requiredFields.length > 0) {
const validation = validateRequiredFields(body, requiredFields);
if (!validation.valid) {
return errorResponse(validation.error as string, 400);
}
}
if (createFunction) {
const result = await createFunction(body);
return createResponse(result);
}
return createResponse({
uuid: Math.floor(Math.random() * 1000),
...body,
});
}
/**
* Generic update operation handler
* @param request NextRequest object
* @param body Request body
* @param updateFunction The function to call to update the item
*/
export async function handleUpdateOperation(
request: NextRequest,
body: any,
updateFunction?: UpdateFunction
) {
const uuid = request.nextUrl.searchParams.get("uuid");
if (!uuid) {
return errorResponse("UUID not found", 400);
}
if (updateFunction) {
const result = await updateFunction(body, uuid);
return updateResponse(result);
}
return updateResponse(body);
}
/**
* Generic delete operation handler
* @param request NextRequest object
* @param deleteFunction The function to call to delete the item
*/
export async function handleDeleteOperation(
request: NextRequest,
deleteFunction?: DeleteFunction
) {
const uuid = request.nextUrl.searchParams.get("uuid");
if (!uuid) {
return errorResponse("UUID not found", 400);
}
if (deleteFunction) {
await deleteFunction(uuid);
}
return deleteResponse();
}
/**
* Create a wrapped list handler with error handling
* @param listFunction The function to call to get the list data
*/
export function createListHandler(listFunction: ListFunction) {
return withErrorHandling((request: NextRequest, body: any) =>
handleListOperation(request, body, listFunction)
);
}
/**
* Create a wrapped create handler with error handling
* @param createFunction The function to call to create the item
* @param requiredFields Array of required field names
*/
export function createCreateHandler(
createFunction?: CreateFunction,
requiredFields: string[] = []
) {
// This handler only takes the body parameter, not the request
return withErrorHandling((body: any) => {
// Ensure we're only passing the actual body data to the create function
if (body && typeof body === 'object' && body.body) {
return handleCreateOperation(body.body, createFunction, requiredFields);
}
return handleCreateOperation(body, createFunction, requiredFields);
});
}
/**
* Create a wrapped update handler with error handling
* @param updateFunction The function to call to update the item
*/
export function createUpdateHandler(updateFunction?: UpdateFunction) {
return withErrorHandling((request: NextRequest, body: any) =>
handleUpdateOperation(request, body, updateFunction)
);
}
/**
* Create a wrapped delete handler with error handling
* @param deleteFunction The function to call to delete the item
*/
export function createDeleteHandler(deleteFunction?: DeleteFunction) {
return withErrorHandling((request: NextRequest) =>
handleDeleteOperation(request, deleteFunction)
);
}

View File

@ -0,0 +1,15 @@
export {
successResponse,
errorResponse,
paginationResponse,
createResponse,
updateResponse,
deleteResponse,
} from "./responseHandlers";
export { withErrorHandling, validateRequiredFields } from "./requestHandlers";
export {
createListHandler,
createCreateHandler,
createUpdateHandler,
createDeleteHandler,
} from "./apiOperations";

View File

@ -0,0 +1,70 @@
import { NextRequest } from "next/server";
import { errorResponse } from "./responseHandlers";
import { ValidationResult, ApiHandler, ApiHandlerBodyOnly, ApiHandlerWithRequest } from "./types";
/**
* Safely parse JSON request body with error handling
* @param request NextRequest object
* @returns Parsed request body or null if parsing fails
*/
export async function parseRequestBody(request: NextRequest) {
try {
return await request.json();
} catch (error) {
return null;
}
}
/**
* Wrapper for API route handlers with built-in error handling
* @param handler The handler function to wrap
*/
export function withErrorHandling(
handler: ApiHandler
) {
return async (request: NextRequest) => {
try {
const body = await parseRequestBody(request);
if (body === null) {
return errorResponse("Invalid request body", 400);
}
// Check handler parameter count to determine if it needs request object
// If handler has only 1 parameter, it's likely a create operation that only needs body
if (handler.length === 1) {
// Cast to the appropriate handler type
return await (handler as ApiHandlerBodyOnly)(body);
} else {
// Otherwise pass both request and body (for list, update, delete operations)
return await (handler as ApiHandlerWithRequest)(request, body);
}
} catch (error: any) {
return errorResponse(
error.message || "Internal Server Error",
error.status || 500
);
}
};
}
/**
* Validate that required fields are present in the request body
* @param body Request body
* @param requiredFields Array of required field names
* @returns Object with validation result and error message if validation fails
*/
export function validateRequiredFields(body: any, requiredFields: string[]): ValidationResult {
const missingFields = requiredFields.filter(field =>
body[field] === undefined || body[field] === null || body[field] === ''
);
if (missingFields.length > 0) {
return {
valid: false,
error: `Missing required fields: ${missingFields.join(', ')}`
};
}
return { valid: true };
}

View File

@ -0,0 +1,91 @@
import { NextResponse } from "next/server";
import { ApiResponse, PaginationResponse, PaginatedApiResponse } from "./types";
/**
* Standard success response handler
* @param data The data to return in the response
* @param status HTTP status code (default: 200)
*/
export function successResponse<T>(data: T, status: number = 200) {
return NextResponse.json(
{
success: true,
data,
} as ApiResponse<T>,
{ status }
);
}
/**
* Standard error response handler
* @param message Error message
* @param status HTTP status code (default: 500)
*/
export function errorResponse(message: string, status: number = 500) {
console.error(`API error: ${message}`);
return NextResponse.json(
{
success: false,
error: message
} as ApiResponse<never>,
{ status }
);
}
/**
* Standard pagination response format
* @param data Array of items to return
* @param pagination Pagination information
*/
export function paginationResponse<T>(data: T[], pagination: PaginationResponse | null) {
return NextResponse.json({
data: data || [],
pagination: pagination || {
page: 1,
size: 10,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: ["name"],
orderType: ["asc"],
query: {},
next: false,
back: false,
},
} as PaginatedApiResponse<T>);
}
/**
* Create response handler
* @param data The created item data
*/
export function createResponse<T>(data: T) {
return successResponse(
{
...data as any,
createdAt: new Date().toISOString(),
} as T,
201
);
}
/**
* Update response handler
* @param data The updated item data
*/
export function updateResponse<T>(data: T) {
return successResponse(
{
...data as any,
updatedAt: new Date().toISOString(),
} as T
);
}
/**
* Delete response handler
*/
export function deleteResponse() {
return successResponse({ message: "Item deleted successfully" }, 204);
}

View File

@ -0,0 +1,119 @@
/**
* Type definitions for API utilities
*/
import { NextRequest } from "next/server";
/**
* Validation result interface
*/
export interface ValidationResult {
valid: boolean;
error?: string;
}
/**
* Pagination parameters interface
*/
export interface PaginationParams {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
/**
* Pagination response interface
*/
export interface PaginationResponse {
page: number;
size: number;
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
next: boolean;
back: boolean;
}
export const defaultPaginationResponse: PaginationResponse = {
page: 1,
size: 10,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: ["uu_id"],
orderType: ["asc"],
query: {},
next: false,
back: false,
};
/**
* API response interface
*/
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
/**
* Paginated API response interface
*/
export interface PaginatedApiResponse<T> {
data: T[];
pagination: PaginationResponse;
}
export const collectPaginationFromApiResponse = (
response: PaginatedApiResponse<any>
): PaginationResponse => {
return {
page: response.pagination?.page || 1,
size: response.pagination?.size || 10,
totalCount: response.pagination?.totalCount || 0,
totalItems: response.pagination?.totalItems || 0,
totalPages: response.pagination?.totalPages || 0,
pageCount: response.pagination?.pageCount || 0,
orderField: response.pagination?.orderField || ["uu_id"],
orderType: response.pagination?.orderType || ["asc"],
query: response.pagination?.query || {},
next: response.pagination?.next || false,
back: response.pagination?.back || false,
};
};
/**
* API handler function types
*/
export type ApiHandlerWithRequest = (request: NextRequest, body: any) => Promise<Response>;
export type ApiHandlerBodyOnly = (body: any) => Promise<Response>;
export type ApiHandler = ApiHandlerWithRequest | ApiHandlerBodyOnly;
/**
* List function type
*/
export type ListFunction = (
params: PaginationParams
) => Promise<PaginatedApiResponse<any>>;
/**
* Create function type
*/
export type CreateFunction = (data: any) => Promise<any>;
/**
* Update function type
*/
export type UpdateFunction = (id: any, data: any) => Promise<any>;
/**
* Delete function type
*/
export type DeleteFunction = (id: any) => Promise<any>;

View File

@ -0,0 +1,44 @@
interface FetcherRequest {
url: string;
isNoCache: boolean;
}
interface PostFetcherRequest<T> extends FetcherRequest {
body: Record<string, T>;
}
interface GetFetcherRequest extends FetcherRequest {
url: string;
}
interface DeleteFetcherRequest extends GetFetcherRequest {}
interface PutFetcherRequest<T> extends PostFetcherRequest<T> {}
interface PatchFetcherRequest<T> extends PostFetcherRequest<T> {}
interface FetcherRespose {
success: boolean;
}
interface PaginationResponse {
onPage: number;
onPageCount: number;
totalPage: number;
totalCount: number;
next: boolean;
back: boolean;
}
interface FetcherDataResponse<T> extends FetcherRespose {
data: Record<string, T> | null;
pagination?: PaginationResponse;
}
export type {
FetcherRequest,
PostFetcherRequest,
GetFetcherRequest,
DeleteFetcherRequest,
PutFetcherRequest,
PatchFetcherRequest,
FetcherRespose,
FetcherDataResponse,
};

View File

@ -4,7 +4,7 @@ import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/mutual/ui/button"
function AlertDialog({
...props

View File

@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/mutual/ui/button"
function Calendar({
className,

View File

@ -11,7 +11,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
} from "@/components/mutual/ui/dialog"
function Command({
className,

View File

@ -14,7 +14,7 @@ import {
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Label } from "@/components/mutual/ui/label"
const Form = FormProvider

Some files were not shown because too many files have changed in this diff Show More