This commit is contained in:
AntoXa PRO 2023-07-11 10:25:49 +03:00
commit e81f6ac8b7
16 changed files with 454 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
package-lock.json

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": true,
"tabWidth": 2,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"arrowParens": "avoid",
"printWidth": 79
}

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "axp-server",
"version": "1.4.12",
"description": "My helper library",
"author": "AntoXa PRO <info@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"
}
}

12
rollup.config.ts Normal file
View File

@ -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()]
})

36
src/app.ts Normal file
View File

@ -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> => {
// 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
}

42
src/config.ts Normal file
View File

@ -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() || ''
}

56
src/core/app-module.ts Normal file
View File

@ -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<void>
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)
}
}
}

54
src/core/controllers.ts Normal file
View File

@ -0,0 +1,54 @@
import { Response } from 'express'
import { DataResultEntity } from 'axp-ts'
/**
* Базовый контроллер.
*/
export class BaseController {
/**
* Отправка результата от сервера.
*/
sendRes<T>(res: Response, dR: DataResultEntity<T>): void {
res.status(dR.status).json(dR)
}
/**
* Выполнение и обработка промиса с действием.
* Всегда положительный результат выполнения промиса.
*/
exec<T>(
fn: () => Promise<T>,
args?: { res?: Response }
): Promise<DataResultEntity<T>> {
return new Promise<DataResultEntity<T>>(async resolve => {
// Модель данных для возврата.
const dR = new DataResultEntity<T>()
// Выполнение действия.
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)
})
}
}

49
src/core/errors.ts Normal file
View File

@ -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'
}
}
}

98
src/core/handlers.ts Normal file
View File

@ -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)
}
}

4
src/core/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './errors'
export * from './app-module'
export * from './handlers'
export * from './controllers'

2
src/helpers/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { default as urlHelper } from './url'
export { default as pathHelper } from './path'

16
src/helpers/path.ts Normal file
View File

@ -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()

15
src/helpers/url.ts Normal file
View File

@ -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()

6
src/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './config'
export * from './helpers'
export * from './core'
import app from './app'
export default app

17
tsconfig.json Normal file
View File

@ -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"]
}