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