add core api
This commit is contained in:
22
src/common/controller.ts
Normal file
22
src/common/controller.ts
Normal 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;
|
||||
}
|
||||
5
src/common/errors/file-not-found.error.ts
Normal file
5
src/common/errors/file-not-found.error.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class FileNotFoundError extends Error {
|
||||
public constructor(fileName: string) {
|
||||
super(`File '${fileName}' not found.`);
|
||||
}
|
||||
}
|
||||
8
src/common/errors/invalid-file-buffer.error.ts
Normal file
8
src/common/errors/invalid-file-buffer.error.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class InvalidFileBufferError extends Error {
|
||||
public constructor(
|
||||
public readonly buffer: Buffer,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
11
src/common/middlewares/orm.middleware.ts
Normal file
11
src/common/middlewares/orm.middleware.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
14
src/common/services/file-storage/abstract.file-storage.ts
Normal file
14
src/common/services/file-storage/abstract.file-storage.ts
Normal 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>;
|
||||
}
|
||||
59
src/common/services/file-storage/local.file-storage.ts
Normal file
59
src/common/services/file-storage/local.file-storage.ts
Normal 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
29
src/common/types.ts
Normal 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;
|
||||
}[];
|
||||
};
|
||||
Reference in New Issue
Block a user