commit e81f6ac8b79e32b201a64661b3529a538614e2b5 Author: AntoXa PRO Date: Tue Jul 11 10:25:49 2023 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16acd49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cdda34f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "tabWidth": 2, + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "arrowParens": "avoid", + "printWidth": 79 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..64210a9 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "axp-server", + "version": "1.4.12", + "description": "My helper library", + "author": "AntoXa PRO ", + "homepage": "https://antoxahub.ru/antoxa/axp-server", + "repository": { + "type": "git", + "url": "https://antoxahub.ru/antoxa/axp-server.git" + }, + "type": "module", + "main": "dist/index.js", + "files": [ + "dist", + "tsconfig.json" + ], + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c --configPlugin @rollup/plugin-typescript", + "prepare": "npm run build" + }, + "dependencies": { + "axp-ts": "^1.9.6", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mongoose": "^6.11.2" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.2", + "@types/express": "^4.17.17", + "prettier": "^2.8.8", + "rollup": "^3.26.2", + "tslib": "^2.6.0" + } +} diff --git a/rollup.config.ts b/rollup.config.ts new file mode 100644 index 0000000..c663d7b --- /dev/null +++ b/rollup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'rollup' +import typescript from '@rollup/plugin-typescript' + +export default defineConfig({ + input: 'src/index.ts', + output: { + dir: 'dist', + format: 'es' + }, + external: ['path', 'axp-ts', 'dotenv', 'express', 'mongoose'], + plugins: [typescript()] +}) diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..27d25d2 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,36 @@ +import type { Express } from 'express' +import express, { Router } from 'express' + +import { config } from './config' +import { AppModule } from './core/app-module' +import { api404Handler, resultHandler } from './core/handlers' + +export default async ( + modules: (typeof AppModule)[] = [] +): Promise => { + // Express. + const app = express() + const apiRouter = Router() + const logPrefix = AppModule.logPrefix + + // Инициализация модулей. + console.log('Инициализация модулей:') + for (const ModuleInstance of modules) { + try { + const module = new ModuleInstance({ app, apiRouter }) + console.log(logPrefix, module.name) + await module.init() + } catch (ex: any) { + console.error(logPrefix, '[X]', 'Error init app module:', ex.message) + } + } + + // Регистрация роутера API. + console.info('Регистрация роутера АПИ') + app.use(config.paths.api, apiRouter) + app.use(config.paths.api + '/*', api404Handler) + app.use(resultHandler) + + // Возвращаем инстанс. + return app +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..569acf4 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,42 @@ +import dotenv from 'dotenv' + +// Конфиг приложения. +export type TConfig = { + isDev: boolean + port: number + baseUrl: string + basePath: string + dirs: { static: string } + paths: { api: string; static: string } + getParam: (name: string) => string +} + +// Инициализация. +dotenv.config() + +// Конфигурация по умолчанию. +const { + NODE_ENV = 'production', + PORT = '4000', + BASE_URL = '/', + BASE_PATH = process.cwd(), + PATH_API = '/api', + PATH_STATIC = '/static', + DIR_STATIC = 'public' +} = process.env + +const isDev = NODE_ENV === 'development' +const port = Number.parseInt(PORT) +const baseUrl = isDev ? 'http://localhost:' + port + '/' : BASE_URL +const basePath = BASE_PATH + +// Кофигурация. +export const config: TConfig = { + isDev, + port, + baseUrl, + basePath, + dirs: { static: DIR_STATIC }, + paths: { api: PATH_API, static: PATH_STATIC }, + getParam: (name: string) => process.env[name]?.toString() || '' +} diff --git a/src/core/app-module.ts b/src/core/app-module.ts new file mode 100644 index 0000000..39bdd26 --- /dev/null +++ b/src/core/app-module.ts @@ -0,0 +1,56 @@ +import { Express, Router } from 'express' + +/** + * Контекст для метода инициализации модуля. + */ +export type TAppModuleInitContext = { + app: Express + apiRouter: Router +} + +/** + * Роутер для регистрации. + */ +export type TRegisterRouter = { + path: string + router: Router +} + +/** + * Интерфейс модуля приложения. + */ +export interface IAppModule { + name: string + init(): Promise + registerRoutes(items: TRegisterRouter[]): void +} + +/** + * Модуль приложения. + */ +export class AppModule implements IAppModule { + name: string = 'Модуль' + static logPrefix: string = '|-' + + protected _context: TAppModuleInitContext + + constructor(context: TAppModuleInitContext) { + this._context = context + } + + /** + * Инициализация модуля. + */ + async init() { + throw new Error(this.name + ' - requires implementation init()') + } + + /** + * Регистрация маршрутов. + */ + registerRoutes(items: TRegisterRouter[]): void { + for (const item of items) { + this._context.apiRouter.use(item.path, item.router) + } + } +} diff --git a/src/core/controllers.ts b/src/core/controllers.ts new file mode 100644 index 0000000..0e15769 --- /dev/null +++ b/src/core/controllers.ts @@ -0,0 +1,54 @@ +import { Response } from 'express' +import { DataResultEntity } from 'axp-ts' + +/** + * Базовый контроллер. + */ +export class BaseController { + /** + * Отправка результата от сервера. + */ + sendRes(res: Response, dR: DataResultEntity): void { + res.status(dR.status).json(dR) + } + + /** + * Выполнение и обработка промиса с действием. + * Всегда положительный результат выполнения промиса. + */ + exec( + fn: () => Promise, + args?: { res?: Response } + ): Promise> { + return new Promise>(async resolve => { + // Модель данных для возврата. + const dR = new DataResultEntity() + + // Выполнение действия. + try { + const data = await fn() + if (data) { + dR.setData(data) + } else { + // Ресурс не найден. + dR.status = 404 + dR.message = 'Not found' + dR.errors.push({ code: 'nod_found', text: 'Resource not found' }) + } + } catch (ex: any) { + // Ошибка сервера. + dR.status = 500 + dR.message = 'Server Error' + dR.errors.push({ code: 'error_exec', text: ex.message }) + } + + // Отправляем ответ сервера. + if (args?.res) { + this.sendRes(args.res, dR) + } + + // Результат. + resolve(dR) + }) + } +} diff --git a/src/core/errors.ts b/src/core/errors.ts new file mode 100644 index 0000000..54aff37 --- /dev/null +++ b/src/core/errors.ts @@ -0,0 +1,49 @@ +import { TNotificationItem } from 'axp-ts' + +/** + * Тип - Http ошибка. + */ +export type THttpError = { + status: number + message: string + errors: TNotificationItem[] +} + +/** + * Http ошибка. + */ +export class HttpError implements THttpError { + status: number = 500 + message: string = 'Server Error' + errors: TNotificationItem[] = [] + + constructor(args?: { text?: string; code?: string; statusCode?: number }) { + this.status = args?.statusCode || 500 + this.message = this.getStatusMessage(this.status) + if (args?.text) { + this.errors.push({ code: args.code || 'error', text: args.text }) + } + } + + /** + * Возвращает название ошибки. + */ + getStatusMessage(status: number) { + const messages = [ + { status: 400, value: 'Validation Error' }, + { status: 401, value: 'Auth Error' }, + { status: 403, value: 'Access Error' }, + { status: 404, value: 'Not found' }, + { status: 500, value: 'Server Error' }, + { status: 520, value: 'Unknown Error' } + ] + + const message = messages.find(e => e.status === status) + + if (message) { + return message.value + } else { + return 'Unknown Error' + } + } +} diff --git a/src/core/handlers.ts b/src/core/handlers.ts new file mode 100644 index 0000000..5eafc3d --- /dev/null +++ b/src/core/handlers.ts @@ -0,0 +1,98 @@ +import { Request, Response, NextFunction } from 'express' +import { z } from 'zod' +import { DataResultEntity } from 'axp-ts' + +import { HttpError } from './errors' + +/** + * Основной обработчик ошибок. + */ +export const resultHandler = ( + result: any, + { }: Request, + res: Response, + { }: NextFunction +) => { + const dR = new DataResultEntity() + + if (result instanceof HttpError) { + dR.status = result.status + dR.message = result.message + dR.errors = result.errors + } else if (result instanceof Error) { + dR.status = 500 + dR.message = 'Server Error' + dR.errors.push({ code: 'server', text: result.message }) + } else { + dR.status = result.status || 520 + dR.message = result.message || 'Unknown Error' + + if (result.info) dR.info = result.info + if (result.data) dR.data = result.data + + const { errors = [] } = result + if (Array.isArray(errors)) { + for (const error of errors) { + const { code = 'error', text } = error + dR.errors.push({ code, text }) + } + } + } + + res.status(dR.status).json(dR) +} + +/** + * Обработчик 404 ошибки. + */ +export const api404Handler = ( + { }: Request, + { }: Response, + next: NextFunction +) => { + next( + new HttpError({ + statusCode: 404, + code: 'not_found', + text: 'Resource api not found' + }) + ) +} + +/** + * Аргументы валидации Zod схем. + */ +export type TZodMiddleArgs = { + query?: z.ZodSchema + params?: z.ZodSchema + body?: z.ZodSchema +} + +/** + * Валидация zod схем. + */ +export const zodMiddle = + (schemas: TZodMiddleArgs) => + (req: Request, {}: Response, next: NextFunction) => { + try { + // req.params._id = '' + // req.body.email = 'test' + // console.log(req.body) + + if (schemas.query) req.query = schemas.query.parse(req.query) + if (schemas.params) req.params = schemas.params.parse(req.params) + if (schemas.body) req.body = schemas.body.parse(req.body) + + next() + } catch (ex: any) { + ex as z.ZodError + const httpError = new HttpError({ statusCode: 400 }) + + for (const issue of ex.issues) { + const code = issue.path.toString().replaceAll(',', '-') + httpError.errors.push({ code, text: code + ' - ' + issue.message }) + } + + next(httpError) + } + } diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..78f9e04 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,4 @@ +export * from './errors' +export * from './app-module' +export * from './handlers' +export * from './controllers' diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 0000000..91c29b2 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,2 @@ +export { default as urlHelper } from './url' +export { default as pathHelper } from './path' diff --git a/src/helpers/path.ts b/src/helpers/path.ts new file mode 100644 index 0000000..51178c6 --- /dev/null +++ b/src/helpers/path.ts @@ -0,0 +1,16 @@ +import path from 'path' +import { config } from '../config' + +/** + * Хелпер PATH. + */ +export class PathHelper { + getPath(str: string): string { + return path.join(config.basePath, str) + } + getPathStatic(str: string): string { + return path.join(this.getPath(config.dirs.static), str) + } +} + +export default new PathHelper() diff --git a/src/helpers/url.ts b/src/helpers/url.ts new file mode 100644 index 0000000..f578565 --- /dev/null +++ b/src/helpers/url.ts @@ -0,0 +1,15 @@ +import { config } from '../config' + +/** + * Хелпер URL. + */ +export class UrlHelper { + getUrl(str: string) { + return config.baseUrl + str.replace(/^\//, '') + } + getUrlStatic(str: string): string { + return this.getUrl(config.paths.static) + '/' + str.replace(/^\//, '') + } +} + +export default new UrlHelper() diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..27df1c3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +export * from './config' +export * from './helpers' +export * from './core' + +import app from './app' +export default app diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9445ee4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ES6", + + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + + "declaration": true + }, + "exclude": ["rollup.config.ts"] +}