init
This commit is contained in:
commit
e81f6ac8b7
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
package-lock.json
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"printWidth": 79
|
||||||
|
}
|
35
package.json
Normal file
35
package.json
Normal 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
12
rollup.config.ts
Normal 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
36
src/app.ts
Normal 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
42
src/config.ts
Normal 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
56
src/core/app-module.ts
Normal 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
54
src/core/controllers.ts
Normal 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
49
src/core/errors.ts
Normal 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
98
src/core/handlers.ts
Normal 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
4
src/core/index.ts
Normal 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
2
src/helpers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as urlHelper } from './url'
|
||||||
|
export { default as pathHelper } from './path'
|
16
src/helpers/path.ts
Normal file
16
src/helpers/path.ts
Normal 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
15
src/helpers/url.ts
Normal 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
6
src/index.ts
Normal 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
17
tsconfig.json
Normal 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"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user