add core api

This commit is contained in:
ItsMalma
2025-11-15 22:28:58 +07:00
parent e6386648be
commit 8f91994f29
78 changed files with 6701 additions and 904 deletions

22
src/common/controller.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { ErrorResponse } from "@/common/types";
import type { Response, Router } from "express";
import type z from "zod";
export abstract class Controller {
protected handleZodError(
zodError: z.ZodError,
res: Response,
location: string = "body",
) {
res.status(422).json({
data: null,
errors: zodError.issues.map((issue) => ({
path: issue.path.join("."),
location,
message: issue.message,
})),
} satisfies ErrorResponse);
}
public abstract buildRouter(): Router;
}

View File

@@ -0,0 +1,5 @@
export class FileNotFoundError extends Error {
public constructor(fileName: string) {
super(`File '${fileName}' not found.`);
}
}

View File

@@ -0,0 +1,8 @@
export class InvalidFileBufferError extends Error {
public constructor(
public readonly buffer: Buffer,
message: string,
) {
super(message);
}
}

View File

@@ -0,0 +1,11 @@
import { orm } from "@/database/orm";
import { RequestContext } from "@mikro-orm/core";
import type { NextFunction, Request, Response } from "express";
export function ormMiddleware(
_req: Request,
_res: Response,
next: NextFunction,
) {
RequestContext.create(orm.em, next);
}

View File

@@ -1,6 +1,19 @@
import { isValid, parse } from "date-fns";
import z from "zod";
export const paginationQuerySchema = z.object({
page: z.coerce
.number("Must be number.")
.int("Must be integer.")
.min(1, "Minimum 1.")
.default(1),
per_page: z.coerce
.number("Must be number.")
.int("Must be integer.")
.min(1, "Minimum 1.")
.default(100),
});
export const timeSchema = z
.string("Must be string.")
.nonempty("Must not be empty.")
@@ -14,21 +27,21 @@ export const timeSchema = z
});
return z.NEVER;
}
return val;
return parsedDate;
});
export const dateSchema = z
.string("Must be string.")
.nonempty("Must not be empty.")
.transform((val, ctx) => {
const parsedDate = parse(val, "YYYY-MM-DD", new Date());
const parsedDate = parse(val, "yyyy-MM-dd", new Date());
if (!isValid(parsedDate)) {
ctx.issues.push({
code: "custom",
message: "Must be in 'YYYY-MM-DD' format.",
message: "Must be in 'yyyy-MM-dd' format.",
input: val,
});
return z.NEVER;
}
return val;
return parsedDate;
});

View File

@@ -0,0 +1,14 @@
export type FileResult = {
name: string;
mimeType: string;
extension: string;
buffer: Buffer;
};
export abstract class AbstractFileStorage {
public abstract storeFile(buffer: Buffer, name?: string): Promise<FileResult>;
public abstract retrieveFile(name: string): Promise<FileResult>;
public abstract removeFile(name: string): Promise<void>;
}

View File

@@ -0,0 +1,59 @@
import { InvalidFileBufferError } from "@/common/errors/invalid-file-buffer.error";
import {
AbstractFileStorage,
type FileResult,
} from "@/common/services/file-storage/abstract.file-storage";
import { fileTypeFromBuffer } from "file-type";
import { join } from "node:path";
const STORAGE_DIRECTORY = join(process.cwd(), "storage");
export class LocalFileStorage extends AbstractFileStorage {
public constructor() {
super();
}
public async storeFile(buffer: Buffer, name?: string): Promise<FileResult> {
const fileType = await fileTypeFromBuffer(buffer);
if (!fileType) {
throw new InvalidFileBufferError(
buffer,
"Unable to determine file type from buffer.",
);
}
const fileName = name ?? `${Date.now()}.${fileType.ext}`;
const filePath = join(STORAGE_DIRECTORY, fileName);
await Bun.write(filePath, buffer, { createPath: true });
return {
name: fileName,
buffer,
mimeType: fileType.mime,
extension: fileType.ext,
};
}
public async retrieveFile(name: string): Promise<FileResult> {
const filePath = join(STORAGE_DIRECTORY, name);
const file = Bun.file(filePath);
const fileName = file.name ?? name;
return {
name: fileName,
buffer: Buffer.from(await file.arrayBuffer()),
mimeType: file.type,
extension: fileName.split(".").pop() ?? "",
};
}
public async removeFile(name: string): Promise<void> {
const filePath = join(STORAGE_DIRECTORY, name);
await Bun.file(filePath).delete();
}
}

29
src/common/types.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { paginationQuerySchema } from "@/common/schemas";
import type z from "zod";
export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
export type SingleResponse<T> = {
data: T;
errors: null;
};
export type ListResponse<T> = {
data: T[];
errors: null;
meta: {
page: number;
per_page: number;
total_pages: number;
total_items: number;
};
};
export type ErrorResponse = {
data: null;
errors: {
path?: string;
location?: string;
message: string;
}[];
};