evaengine 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/defaults/ai-coding.md +47 -0
- package/.ai/defaults/preferences.md +45 -0
- package/.ai/memory.md +38 -0
- package/.ai/workflow/design-review.md +38 -0
- package/.ai/workflow/end.md +34 -0
- package/.ai/workflow/start.md +34 -0
- package/.ai/workflow/sync.md +24 -0
- package/README.md +384 -54
- package/bin/engine +1 -1
- package/package.json +2 -44
- package/src/commands/make_entity.js +11 -11
- package/src/di.js +13 -1
- package/src/engine.js +12 -9
- package/src/index.js +1 -3
- package/src/middlewares/session.js +6 -1
- package/src/middlewares/trace.js +26 -1
- package/src/middlewares/view_cache.js +3 -2
- package/src/services/config.js +10 -6
- package/src/services/http_client.js +3 -94
- package/src/services/jwt_token_kong.js +2 -2
- package/src/services/redis.js +9 -1
- package/src/swagger/index.js +1 -6
- package/src/utils/cron.js +208 -0
- package/src/utils/pagination.js +1 -1
- package/src/utils/request_client.js +179 -0
- package/.ai/analysis.md +0 -657
- package/.ai/architecture/runtime.md +0 -504
- package/.ai/contracts/public-api.md +0 -447
- package/.ai/external-project-guide.md +0 -248
- package/.ai/vision.md +0 -314
package/README.md
CHANGED
|
@@ -2,113 +2,443 @@
|
|
|
2
2
|
|
|
3
3
|
[](http://badge.fury.io/js/evaengine)
|
|
4
4
|
[](https://github.com/EvaEngine/EvaEngine.js/actions/workflows/ci.yml)
|
|
5
|
-
[](https://codecov.io/gh/EvaEngine/EvaEngine.js)
|
|
6
6
|
[](https://www.npmjs.com/package/evaengine)
|
|
7
|
-
[](https://github.com/EvaEngine/EvaEngine.js/blob/
|
|
7
|
+
[](https://github.com/EvaEngine/EvaEngine.js/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
面向 Node.js 微服务的 **Application Runtime**:同一套引擎覆盖 **HTTP**、**CLI**、**定时任务**,并提供 DI、Provider、中间件、配置、缓存、鉴权辅助、实体(Sequelize)、异常体系与 Swagger 生成。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **消费方(人与 agent):** 本 README 即为完整对外说明。使用本包**不需要**阅读仓库内的 `docs/`。
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- npm
|
|
13
|
+
## 环境要求
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
- Node.js **≥ 24**
|
|
16
|
+
- ESM(`"type": "module"`)
|
|
17
|
+
- npm(或其它可从 npm registry 安装的客户端)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
## 安装
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
22
|
npm install evaengine
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
可选脚手架:[EvaSkeleton.js](https://github.com/EvaEngine/EvaSkeleton.js)。
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
## 导入方式(重要)
|
|
28
|
+
|
|
29
|
+
包的 **default 导出** 是一个 `core` 对象。顶层具名导出只有 `default` 与 `core`(同一对象)。
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import eva from 'evaengine';
|
|
33
|
+
// 或:import { core as eva } from 'evaengine';
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
EvaEngine,
|
|
37
|
+
Command,
|
|
38
|
+
DI,
|
|
39
|
+
Entities,
|
|
40
|
+
express,
|
|
41
|
+
wrapper,
|
|
42
|
+
services,
|
|
43
|
+
middlewares,
|
|
44
|
+
providers,
|
|
45
|
+
exceptions,
|
|
46
|
+
swagger,
|
|
47
|
+
utils,
|
|
48
|
+
commands, // 内置 CLI 命令
|
|
49
|
+
Joi,
|
|
50
|
+
sequelize
|
|
51
|
+
} = eva;
|
|
30
52
|
```
|
|
31
53
|
|
|
32
|
-
|
|
54
|
+
**不要**依赖 `import { EvaEngine } from 'evaengine'`——该具名导出不存在。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 心智模型
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
new EvaEngine(meta, mode?)
|
|
62
|
+
→ 绑定 base 服务(env, config, logger, namespace, now, event_manager)
|
|
63
|
+
→ bootstrap() # web 服务 + 中间件 Provider
|
|
64
|
+
→ use(...) / registerCommands
|
|
65
|
+
→ run() | runHttps() | runCLI() | runCrontab() | runCommand()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| 模式 | 典型流程 |
|
|
69
|
+
|------|----------|
|
|
70
|
+
| `web`(默认) | `bootstrap()` → `use()` → `run()` / `runHttps()` |
|
|
71
|
+
| `cli` | `registerCommands()` → `runCLI()` / `runCrontab()` / `runCommand()` |
|
|
72
|
+
|
|
73
|
+
**进程级事实(按每进程一个 Engine 规划):**
|
|
33
74
|
|
|
34
|
-
|
|
75
|
+
- `DI` 是**全局**容器。
|
|
76
|
+
- `EvaEngine.getApp()` 是**模块级** Express app 单例。
|
|
77
|
+
- `bootstrap()` 注册 **web** 服务与中间件 Provider;CLI 路径在 `getCLI` / `runCrontab` 内注册 CLI 服务。
|
|
78
|
+
- 内置 `EventManager` **仅进程内**(不是消息队列)。
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 快速开始
|
|
83
|
+
|
|
84
|
+
### Web
|
|
35
85
|
|
|
36
86
|
```js
|
|
37
|
-
import
|
|
87
|
+
import eva from 'evaengine';
|
|
88
|
+
|
|
89
|
+
const { EvaEngine, DI, wrapper, exceptions } = eva;
|
|
90
|
+
const { UnauthorizedException } = exceptions;
|
|
38
91
|
|
|
39
92
|
const engine = new EvaEngine({
|
|
40
93
|
projectRoot: process.cwd(),
|
|
41
|
-
port: 3000
|
|
94
|
+
port: Number(process.env.PORT) || 3000
|
|
95
|
+
// configPath、sourceRoot 可选
|
|
42
96
|
});
|
|
43
97
|
|
|
44
98
|
engine.bootstrap();
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
|
|
100
|
+
// 可选横切中间件(须在 bootstrap 之后)
|
|
101
|
+
engine.use(DI.get('trace')());
|
|
102
|
+
// engine.use(DI.get('session')());
|
|
103
|
+
// engine.use(DI.get('auth')());
|
|
104
|
+
|
|
105
|
+
engine.use('/health', (req, res) => {
|
|
106
|
+
res.json({ ok: true });
|
|
47
107
|
});
|
|
108
|
+
|
|
109
|
+
engine.use('/me', wrapper(async (req, res) => {
|
|
110
|
+
if (!req.auth?.uid) {
|
|
111
|
+
throw new UnauthorizedException('Login required');
|
|
112
|
+
}
|
|
113
|
+
res.json({ uid: req.auth.uid });
|
|
114
|
+
}));
|
|
115
|
+
|
|
48
116
|
engine.run();
|
|
49
117
|
```
|
|
50
118
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### Run as a CLI app
|
|
119
|
+
### CLI
|
|
54
120
|
|
|
55
121
|
```js
|
|
56
|
-
import
|
|
122
|
+
import eva from 'evaengine';
|
|
57
123
|
import * as UserCommands from './commands/user.js';
|
|
58
124
|
|
|
59
|
-
const
|
|
60
|
-
projectRoot: process.cwd()
|
|
61
|
-
}, 'cli');
|
|
125
|
+
const { EvaEngine } = eva;
|
|
62
126
|
|
|
127
|
+
const engine = new EvaEngine({ projectRoot: process.cwd() }, 'cli');
|
|
63
128
|
engine.registerCommands(UserCommands);
|
|
64
|
-
|
|
65
129
|
await engine.runCLI();
|
|
130
|
+
// node app.js user:create --name=Ada
|
|
66
131
|
```
|
|
67
132
|
|
|
68
|
-
###
|
|
133
|
+
### 定时任务
|
|
69
134
|
|
|
70
135
|
```js
|
|
71
|
-
import
|
|
72
|
-
import * as
|
|
136
|
+
import eva from 'evaengine';
|
|
137
|
+
import * as Jobs from './commands/jobs.js';
|
|
73
138
|
|
|
74
|
-
const
|
|
75
|
-
projectRoot: process.cwd()
|
|
76
|
-
}, 'cli');
|
|
139
|
+
const { EvaEngine } = eva;
|
|
77
140
|
|
|
78
|
-
engine.
|
|
141
|
+
const engine = new EvaEngine({ projectRoot: process.cwd() }, 'cli');
|
|
142
|
+
engine.registerCommands([Jobs]);
|
|
143
|
+
// 六段 cron(含秒)等细节见 runCrontab 第三参数 useSeconds
|
|
79
144
|
engine.runCrontab('0/10 * * * * *', 'hello:world --id=EvaEngine');
|
|
80
145
|
```
|
|
81
146
|
|
|
82
|
-
|
|
147
|
+
### 内置 CLI 二进制
|
|
83
148
|
|
|
84
149
|
```bash
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
150
|
+
npx engine
|
|
151
|
+
npx engine make:entity
|
|
152
|
+
npx engine make:dbview
|
|
153
|
+
npx engine make:graphql
|
|
154
|
+
npx engine tramp:dump-config
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
配合 Spring Cloud Config(仅 bin):
|
|
158
|
+
|
|
159
|
+
- `SPRING_CONFIG_ENDPOINT`(设置后启用)
|
|
160
|
+
- `SPRING_CONFIG_NAME`、`SPRING_CONFIG_PROFILES`、`SPRING_CONFIG_LABEL`
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 推荐项目结构
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
project/
|
|
168
|
+
package.json # "type": "module"
|
|
169
|
+
config/
|
|
170
|
+
config.default.cjs
|
|
171
|
+
config.development.cjs
|
|
172
|
+
config.production.cjs
|
|
173
|
+
config.local.development.cjs # 本地覆盖,建议 gitignore
|
|
174
|
+
src/
|
|
175
|
+
app.js # web 入口
|
|
176
|
+
cli.js
|
|
177
|
+
commands/
|
|
178
|
+
entities/
|
|
179
|
+
routes/
|
|
180
|
+
test/
|
|
88
181
|
```
|
|
89
182
|
|
|
90
|
-
|
|
183
|
+
---
|
|
91
184
|
|
|
92
|
-
|
|
185
|
+
## 配置
|
|
93
186
|
|
|
94
|
-
|
|
95
|
-
- `PORT`
|
|
96
|
-
- `LOG_LEVEL`
|
|
97
|
-
- `CLI_NAME`
|
|
98
|
-
- `MAX_REQUEST_DEBUG_BODY`
|
|
99
|
-
- `SEQUELIZE_REPLICATION_CONFIG_KEY`
|
|
187
|
+
配置目录为 `{projectRoot}/config`(可用构造参数 `configPath` 覆盖),按以下顺序合并:
|
|
100
188
|
|
|
101
|
-
|
|
189
|
+
1. 引擎内置默认(随包提供)
|
|
190
|
+
2. `config.default.cjs`
|
|
191
|
+
3. `config.<NODE_ENV>.cjs`
|
|
192
|
+
4. 可选 `config.local.<NODE_ENV>.cjs`(不存在则忽略)
|
|
102
193
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
194
|
+
配置文件使用 **CommonJS** `.cjs`(经 `require` 加载)。
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
// config/config.default.cjs
|
|
198
|
+
module.exports = {
|
|
199
|
+
app: { name: 'my-service' },
|
|
200
|
+
redis: { host: '127.0.0.1', port: 6379, lazyConnect: true },
|
|
201
|
+
cache: { prefix: 'myapp', driver: 'redis' },
|
|
202
|
+
token: {
|
|
203
|
+
secret: process.env.TOKEN_SECRET || '',
|
|
204
|
+
provider: undefined, // 设为 'kong' 时使用 Kong JWT 与对应 auth 中间件
|
|
205
|
+
faker: { enable: false, key: 'eva', uid: 1 }
|
|
206
|
+
},
|
|
207
|
+
session: {
|
|
208
|
+
secret: process.env.SESSION_SECRET || 'change-me',
|
|
209
|
+
resave: true,
|
|
210
|
+
saveUninitialized: true,
|
|
211
|
+
cookie: { path: '/', httpOnly: true, secure: false, maxAge: 3600_000 }
|
|
212
|
+
},
|
|
213
|
+
db: {
|
|
214
|
+
dialect: 'mysql',
|
|
215
|
+
port: 3306,
|
|
216
|
+
database: '',
|
|
217
|
+
replication: {
|
|
218
|
+
write: { host: '', username: '', password: '', pool: {} },
|
|
219
|
+
read: []
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
运行时读取:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const config = DI.get('config');
|
|
229
|
+
config.get('redis.host');
|
|
230
|
+
config.get(); // 完整对象
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 环境变量
|
|
234
|
+
|
|
235
|
+
| 变量 | 作用 |
|
|
236
|
+
|------|------|
|
|
237
|
+
| `NODE_ENV` | 选择 `config.<env>.cjs` |
|
|
238
|
+
| `PORT` | 常见应用端口(使用时传入构造参数) |
|
|
239
|
+
| `LOG_LEVEL` | 覆盖日志级别 |
|
|
240
|
+
| `TZ` | moment 默认时区(未设置时为 `Asia/Shanghai`) |
|
|
241
|
+
| `CLI_NAME` | CLI 模式下 logger 标签 |
|
|
242
|
+
| `MAX_REQUEST_DEBUG_BODY` | debug 中间件 body 限制 |
|
|
243
|
+
| `SEQUELIZE_REPLICATION_CONFIG_KEY` | `db` 下 replication 配置的替代键名 |
|
|
244
|
+
| `SPRING_CONFIG_*` | bin 远程配置(见上文) |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## DI 与服务
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
DI.get('logger').info('hello');
|
|
252
|
+
DI.get('redis').getInstance();
|
|
253
|
+
DI.get('cache'); // 缓存门面
|
|
254
|
+
DI.get('jwt');
|
|
255
|
+
DI.get('http_client');
|
|
256
|
+
DI.get('rest_client');
|
|
257
|
+
DI.get('event_manager');
|
|
258
|
+
DI.get('namespace');
|
|
259
|
+
DI.get('now');
|
|
260
|
+
DI.get('env');
|
|
261
|
+
DI.get('validator_base');
|
|
106
262
|
```
|
|
107
263
|
|
|
108
|
-
|
|
264
|
+
| DI 名 | 绑定时机 |
|
|
265
|
+
|-------|----------|
|
|
266
|
+
| `env`、`config`、`logger`、`namespace`、`now`、`event_manager` | 构造时(base) |
|
|
267
|
+
| `redis`、`cache`、`http_client`、`rest_client`、`validator_base`、`jwt` | `bootstrap()`(web)或 CLI 执行路径 |
|
|
268
|
+
| 下文中间件名 | `bootstrap()` |
|
|
269
|
+
|
|
270
|
+
**自定义 Provider:**
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
import eva from 'evaengine';
|
|
274
|
+
|
|
275
|
+
const { DI, providers } = eva;
|
|
276
|
+
const { ServiceProvider } = providers.services;
|
|
277
|
+
|
|
278
|
+
class MyApiProvider extends ServiceProvider {
|
|
279
|
+
get name() { return 'my_api'; }
|
|
280
|
+
register() {
|
|
281
|
+
DI.bindValue(this.name, { ping: () => 'pong' });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
engine.registerService(MyApiProvider);
|
|
286
|
+
// 或替换整表:
|
|
287
|
+
// EvaEngine.setServiceProvidersForWeb([...EvaEngine.getServiceProvidersForWeb(), MyApiProvider]);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
测试辅助:`DI.reset()`、`DI.registerMockedProviders(providers, configPath)`、`DI.bindClass` / `bindValue` / `bindMethod`。
|
|
291
|
+
|
|
292
|
+
---
|
|
109
293
|
|
|
110
|
-
|
|
294
|
+
## 中间件
|
|
111
295
|
|
|
112
|
-
|
|
296
|
+
`bootstrap()` 之后按名称绑定工厂。**需要调用工厂**(注意部分场景二次调用):
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
engine.use(DI.get('trace')());
|
|
300
|
+
engine.use(DI.get('session')());
|
|
301
|
+
engine.use(DI.get('auth')());
|
|
302
|
+
// validator 是高阶工厂:
|
|
303
|
+
engine.use('/items', DI.get('validator')(() => ({
|
|
304
|
+
query: eva.Joi.object({ page: eva.Joi.number().integer().required() })
|
|
305
|
+
})), handler);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
| 名称 | 作用 |
|
|
309
|
+
|------|------|
|
|
310
|
+
| `session` | `express-session`(经 connect-redis 的 Redis 存储) |
|
|
311
|
+
| `auth` | 从 `X-Token` 或 `api_key` 取 JWT,或 session `uid`;可选 faker token |
|
|
312
|
+
| `trace` | 请求追踪(与 namespace 协作) |
|
|
313
|
+
| `validator` | Joi 请求校验 |
|
|
314
|
+
| `view_cache` | 响应缓存辅助 |
|
|
315
|
+
| `debug` | 调试输出 |
|
|
316
|
+
|
|
317
|
+
当 `config.token.provider === 'kong'` 时,`jwt` 服务与 `auth` 中间件均切换为 Kong 实现。
|
|
318
|
+
|
|
319
|
+
使用 `wrapper(async (req,res) => …)`,以便抛出的 `exceptions.*` 进入默认错误处理器。
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 命令(Command)
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
import eva from 'evaengine';
|
|
327
|
+
|
|
328
|
+
const { Command, DI } = eva;
|
|
329
|
+
|
|
330
|
+
export class HelloWorld extends Command {
|
|
331
|
+
static getName() { return 'hello:world'; }
|
|
332
|
+
static getDescription() { return 'Say hello'; }
|
|
333
|
+
static getSpec() {
|
|
334
|
+
return {
|
|
335
|
+
id: { type: 'string', description: 'Who to greet' }
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async run() {
|
|
339
|
+
const { id = 'world' } = this.getOptions();
|
|
340
|
+
DI.get('logger').info(`Hello ${id}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
通过 `engine.registerCommands(moduleExports)` 或模块数组注册。名称来自 `getName()`。
|
|
346
|
+
|
|
347
|
+
Engine API:`runCLI()`、`runCommand('name --flag=1')`、`runCrontab(expression, 'name --flag=1', useSeconds?)`、`clearCommands()`、`clearCrontabs()`。
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 实体(Sequelize)
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
import path from 'path';
|
|
355
|
+
import eva from 'evaengine';
|
|
356
|
+
|
|
357
|
+
const { Entities, DI } = eva;
|
|
358
|
+
|
|
359
|
+
const entities = new Entities(path.join(process.cwd(), 'src/entities'));
|
|
360
|
+
entities.init(); // 按 config.db 构建 Sequelize 并扫描目录
|
|
361
|
+
|
|
362
|
+
const User = entities.get('user');
|
|
363
|
+
const all = entities.getAll();
|
|
364
|
+
await entities.getTransaction(async (t) => { /* … */ });
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
实体文件(经 `require` 加载的 CJS 或 ESM 工厂):
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
// src/entities/user.cjs
|
|
371
|
+
module.exports = (sequelize, DataTypes) =>
|
|
372
|
+
sequelize.define('user', {
|
|
373
|
+
id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true },
|
|
374
|
+
name: { type: DataTypes.STRING, allowNull: false }
|
|
375
|
+
}, { tableName: 'users' });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 异常
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
import eva from 'evaengine';
|
|
384
|
+
|
|
385
|
+
const {
|
|
386
|
+
StandardException,
|
|
387
|
+
LogicException,
|
|
388
|
+
InvalidArgumentException,
|
|
389
|
+
UnauthorizedException,
|
|
390
|
+
ResourceNotFoundException,
|
|
391
|
+
RuntimeException
|
|
392
|
+
// …完整列表见包导出 exceptions
|
|
393
|
+
} = eva.exceptions;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
默认 HTTP 错误处理器在 `run` / `runHttps` 时挂载:将 `StandardException` 子类映射为 JSON 与状态码;生产环境会剥离 stack 等细节。
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Swagger
|
|
401
|
+
|
|
402
|
+
使用 `eva.swagger`(`ExSwagger`、注解辅助等)从源码注释与模型生成 Swagger 2.0。在应用脚本中自行接入生成流程;UI 资源来自依赖 `swagger-ui-dist`。
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## EvaEngine API 一览
|
|
407
|
+
|
|
408
|
+
```text
|
|
409
|
+
constructor({ projectRoot, configPath?, sourceRoot?, port?, config?, logger?, namespace? }, mode?='web')
|
|
410
|
+
getMeta() getDI()
|
|
411
|
+
bootstrap() use(...args) run(port?) runHttps(port?, options?) getServer()
|
|
412
|
+
registerCommands(commands) getCommands() clearCommands() getCommand() getCommandName()
|
|
413
|
+
runCLI(name?) runCommand(commandString) runCrontab(seq, commandString, useSeconds?) clearCrontabs()
|
|
414
|
+
registerServiceProviders(providers) registerService(ProviderClass)
|
|
415
|
+
setDefaultErrorHandler / getDefaultErrorHandler
|
|
416
|
+
setUncaughtExceptionHandler / getUncaughtExceptionHandler
|
|
417
|
+
setServerErrorHandler / getServerErrorHandler
|
|
418
|
+
static getApp() createRouter() getVersion()
|
|
419
|
+
static get/set BaseServiceProviders | ServiceProvidersForWeb | ServiceProvidersForCLI | MiddlewareProviders
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 本库不是什么
|
|
425
|
+
|
|
426
|
+
- 不只是 Express 薄封装——HTTP 只是入口之一
|
|
427
|
+
- 不是业务领域框架或业务规则层
|
|
428
|
+
- 不是消息总线(可靠投递请用真正的 MQ)
|
|
429
|
+
- 不是完整 ORM 产品——仅提供 Sequelize 集成辅助
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 本仓库开发
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
git clone https://github.com/EvaEngine/EvaEngine.js.git
|
|
437
|
+
cd EvaEngine.js
|
|
438
|
+
npm install
|
|
439
|
+
npm run lint
|
|
440
|
+
npm run build
|
|
441
|
+
npm test # 部分测试需要本机 Redis 127.0.0.1:6379
|
|
442
|
+
```
|
|
113
443
|
|
|
114
|
-
|
|
444
|
+
发版:在 `main` 上由 semantic-release(Conventional Commits)执行——仅发布 npm,不创建 GitHub Release。维护者文档在 git 的 `docs/` 下,**npm 消费方无需阅读**。
|
package/bin/engine
CHANGED
|
@@ -13,7 +13,7 @@ try {
|
|
|
13
13
|
endpoint: process.env.SPRING_CONFIG_ENDPOINT,
|
|
14
14
|
name: process.env.SPRING_CONFIG_NAME || 'unknown-spring-config-name',
|
|
15
15
|
profiles: process.env.SPRING_CONFIG_PROFILES || process.env.NODE_ENV,
|
|
16
|
-
label: process.env.SPRING_CONFIG_LABEL || '
|
|
16
|
+
label: process.env.SPRING_CONFIG_LABEL || 'main'
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
await engine.runCLI();
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evaengine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "EvaEngine for Node.js, a micro service development engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"package-lock": false,
|
|
8
7
|
"repository": {
|
|
9
8
|
"type": "git",
|
|
10
9
|
"url": "https://github.com/EvaEngine/EvaEngine.js.git"
|
|
@@ -12,8 +11,7 @@
|
|
|
12
11
|
"scripts": {
|
|
13
12
|
"prestart": "node src/index.js",
|
|
14
13
|
"prepublish": "npm run test",
|
|
15
|
-
"
|
|
16
|
-
"test": "nyc -a --reporter=lcov --reporter=text --reporter=html npm run ava",
|
|
14
|
+
"test": "LOG_LEVEL=error NODE_ENV=test node --import ./test/bootstrap.js --test --test-concurrency=1 --experimental-test-coverage \"test/di.js\" \"test/engine.js\" \"test/error_handlers.js\" \"test/index.js\" \"test/exceptions/index.js\" \"test/entities/index.js\" \"test/commands/make_entity.js\" \"test/middlewares/*.js\" \"test/services/*.js\" \"test/swagger/index.js\" \"test/utils/*.js\"",
|
|
17
15
|
"lint": "eslint src test index.js",
|
|
18
16
|
"build": "node --check src/index.js",
|
|
19
17
|
"ci:check": "npm run lint && npm run build",
|
|
@@ -38,31 +36,6 @@
|
|
|
38
36
|
"lint"
|
|
39
37
|
],
|
|
40
38
|
"author": "AlloVince",
|
|
41
|
-
"ava": {
|
|
42
|
-
"files": [
|
|
43
|
-
"test/**/*.js",
|
|
44
|
-
"!test/bootstrap.js",
|
|
45
|
-
"!test/_*/**/*.js",
|
|
46
|
-
"!test/swagger/_*/**/*.js"
|
|
47
|
-
],
|
|
48
|
-
"source": [
|
|
49
|
-
"src/**/*.js"
|
|
50
|
-
],
|
|
51
|
-
"failFast": false,
|
|
52
|
-
"tap": false,
|
|
53
|
-
"require": [
|
|
54
|
-
"./test/bootstrap.js"
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
"nyc": {
|
|
58
|
-
"require": [],
|
|
59
|
-
"include": [
|
|
60
|
-
"src/**/*.js"
|
|
61
|
-
],
|
|
62
|
-
"exclude": [
|
|
63
|
-
"src/config/index.js"
|
|
64
|
-
]
|
|
65
|
-
},
|
|
66
39
|
"dependencies": {
|
|
67
40
|
"acorn": "^8.18.0",
|
|
68
41
|
"app-root-path": "^3.1.0",
|
|
@@ -73,24 +46,15 @@
|
|
|
73
46
|
"doctrine": "^3.0.0",
|
|
74
47
|
"express": "^5.2.1",
|
|
75
48
|
"express-session": "^1.19.0",
|
|
76
|
-
"glob": "^13.0.6",
|
|
77
49
|
"ioredis": "^5.11.1",
|
|
78
50
|
"joi": "^18.2.3",
|
|
79
51
|
"js-yaml": "^5.2.2",
|
|
80
52
|
"jwt-simple": "^0.5.6",
|
|
81
|
-
"later": "^1.2.0",
|
|
82
53
|
"lodash": "^4.18.1",
|
|
83
|
-
"mkdirp": "^3.0.1",
|
|
84
|
-
"moment": "^2.30.1",
|
|
85
54
|
"moment-timezone": "^0.6.3",
|
|
86
55
|
"morgan": "^1.11.0",
|
|
87
|
-
"mysql": "^2.18.1",
|
|
88
56
|
"mysql2": "^3.23.2",
|
|
89
57
|
"node-mocks-http": "~1.18.1",
|
|
90
|
-
"on-headers": "^1.1.0",
|
|
91
|
-
"pug": "^3.0.4",
|
|
92
|
-
"request": "^2.88.2",
|
|
93
|
-
"request-promise-native": "^1.0.9",
|
|
94
58
|
"sequelize": "^6.37.8",
|
|
95
59
|
"swagger-ui-dist": "^5.32.11",
|
|
96
60
|
"winston": "^3.19.0",
|
|
@@ -101,18 +65,12 @@
|
|
|
101
65
|
"@semantic-release/changelog": "^6.0.0",
|
|
102
66
|
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
103
67
|
"@semantic-release/git": "^10.0.0",
|
|
104
|
-
"@semantic-release/github": "^11.0.0",
|
|
105
68
|
"@semantic-release/npm": "^12.0.0",
|
|
106
69
|
"@semantic-release/release-notes-generator": "^14.0.0",
|
|
107
|
-
"ava": "^8.0.1",
|
|
108
70
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
109
|
-
"cross-env": "^10.1.0",
|
|
110
71
|
"eslint": "^9.39.5",
|
|
111
72
|
"eslint-plugin-import": "^2.32.0",
|
|
112
73
|
"globals": "^17.8.0",
|
|
113
|
-
"is-type-of": "^2.2.0",
|
|
114
|
-
"nock": "^14.0.17",
|
|
115
|
-
"nyc": "^18.0.0",
|
|
116
74
|
"pre-commit": "^2.0.0",
|
|
117
75
|
"semantic-release": "^25.0.8"
|
|
118
76
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import groupBy from 'lodash/groupBy.js';
|
|
2
|
+
import template from 'lodash/template.js';
|
|
2
3
|
import Sequelize from 'sequelize';
|
|
3
4
|
import fs from 'fs';
|
|
4
|
-
import * as mkdirp from 'mkdirp';
|
|
5
5
|
import Command from './interface.js';
|
|
6
6
|
import DI from '../di.js';
|
|
7
7
|
import Entities from '../entities/index.js';
|
|
@@ -55,7 +55,7 @@ CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW view_${tableName}
|
|
|
55
55
|
const entities = new Entities(entityPath);
|
|
56
56
|
|
|
57
57
|
logger.info('Start generate db views to %s', file);
|
|
58
|
-
|
|
58
|
+
fs.mkdirSync(path, { recursive: true });
|
|
59
59
|
const models = Object.values(entities.getInstance().models);
|
|
60
60
|
const sql = [];
|
|
61
61
|
models.forEach((model) => {
|
|
@@ -202,7 +202,7 @@ export class MakeEntity extends Command {
|
|
|
202
202
|
if (!rawIndexes) {
|
|
203
203
|
return [];
|
|
204
204
|
}
|
|
205
|
-
rawIndexes =
|
|
205
|
+
rawIndexes = groupBy(rawIndexes, 'Key_name');
|
|
206
206
|
return Object.entries(rawIndexes).filter(([key]) => key !== 'PRIMARY').map(([name, columns]) => {
|
|
207
207
|
const index = columns[0].Non_unique !== 1 ? { name, unique: true } : { name };
|
|
208
208
|
index.fields = columns.map(c => c.Column_name);
|
|
@@ -239,8 +239,8 @@ export class MakeEntity extends Command {
|
|
|
239
239
|
const schemaPath = `${path}/schemas`;
|
|
240
240
|
const entityTemplate = fs.readFileSync(`${import.meta.dirname}/../../template/entity.ejs`, 'utf8');
|
|
241
241
|
const schemaTemplate = fs.readFileSync(`${import.meta.dirname}/../../template/schema.ejs`, 'utf8');
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
fs.mkdirSync(path, { recursive: true });
|
|
243
|
+
fs.mkdirSync(schemaPath, { recursive: true });
|
|
244
244
|
|
|
245
245
|
logger.info('Start generate DB schemas to dir %s', path);
|
|
246
246
|
|
|
@@ -269,7 +269,7 @@ export class MakeEntity extends Command {
|
|
|
269
269
|
fs.accessSync(entityFile);
|
|
270
270
|
logger.info('Entity file %s generate skipped, already exists by %s', table, entityFile);
|
|
271
271
|
} catch {
|
|
272
|
-
fs.writeFileSync(entityFile,
|
|
272
|
+
fs.writeFileSync(entityFile, template(entityTemplate)({ table }));
|
|
273
273
|
logger.info('Entity file %s generated as %s', table, entityFile);
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -279,7 +279,7 @@ export class MakeEntity extends Command {
|
|
|
279
279
|
} catch {
|
|
280
280
|
logger.info('Schema file %s generated as %s', table, schemaFile);
|
|
281
281
|
}
|
|
282
|
-
fs.writeFileSync(schemaFile,
|
|
282
|
+
fs.writeFileSync(schemaFile, template(schemaTemplate)({
|
|
283
283
|
columns,
|
|
284
284
|
table,
|
|
285
285
|
indexes,
|
|
@@ -438,8 +438,8 @@ export class MakeGraphql extends Command {
|
|
|
438
438
|
const mappingContent = JSON.parse(fs.readFileSync(mappingFile, 'utf8'));
|
|
439
439
|
const getMappedTableName = tableName =>
|
|
440
440
|
(mappingContent[tableName] ? mappingContent[tableName] : tableName);
|
|
441
|
-
|
|
442
|
-
|
|
441
|
+
fs.mkdirSync(path, { recursive: true });
|
|
442
|
+
fs.mkdirSync(schemaPath, { recursive: true });
|
|
443
443
|
|
|
444
444
|
logger.info('Start generate GraphQL schemas to dir %s', path);
|
|
445
445
|
|
|
@@ -474,7 +474,7 @@ export class MakeGraphql extends Command {
|
|
|
474
474
|
} catch {
|
|
475
475
|
logger.info('Graphql schema file %s generated as %s', tableName, schemaFile);
|
|
476
476
|
}
|
|
477
|
-
fs.writeFileSync(schemaFile,
|
|
477
|
+
fs.writeFileSync(schemaFile, template(schemaTemplate)({
|
|
478
478
|
tableName,
|
|
479
479
|
mappedTableName,
|
|
480
480
|
columns,
|