@wneng/create-keel 0.3.4 → 0.4.0
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/dist/index.js +211 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/ci-gitee/files/PULL_REQUEST_TEMPLATE.md +13 -1
- package/src/templates/ci-gitee/files/pipeline.yml +71 -0
- package/src/templates/ci-github/files/PULL_REQUEST_TEMPLATE.md +13 -1
- package/src/templates/ci-github/files/ci.yml +169 -0
- package/src/templates/contracts-events/fragment.yaml +1 -1
- package/src/templates/contracts-rest-events/fragment.yaml +1 -1
- package/src/templates/db-go-elasticsearch/files/Makefile +18 -0
- package/src/templates/db-go-elasticsearch/files/apply_templates.go +80 -0
- package/src/templates/db-go-elasticsearch/files/db-README.md +57 -0
- package/src/templates/db-go-elasticsearch/files/go.mod +7 -0
- package/src/templates/db-go-elasticsearch/files/index-template-init.json +25 -0
- package/src/templates/db-go-elasticsearch/fragment.yaml +22 -0
- package/src/templates/db-go-migrate-mysql/files/000001_init.down.sql +3 -0
- package/src/templates/db-go-migrate-mysql/files/000001_init.up.sql +35 -0
- package/src/templates/db-go-migrate-mysql/files/Makefile +33 -0
- package/src/templates/db-go-migrate-mysql/files/db-README.md +77 -0
- package/src/templates/db-go-migrate-mysql/files/go.mod +8 -0
- package/src/templates/db-go-migrate-mysql/fragment.yaml +22 -0
- package/src/templates/db-go-migrate-postgres/files/000001_init.down.sql +3 -0
- package/src/templates/db-go-migrate-postgres/files/000001_init.up.sql +32 -0
- package/src/templates/db-go-migrate-postgres/files/Makefile +31 -0
- package/src/templates/db-go-migrate-postgres/files/db-README.md +71 -0
- package/src/templates/db-go-migrate-postgres/files/go.mod +8 -0
- package/src/templates/db-go-migrate-postgres/fragment.yaml +22 -0
- package/src/templates/db-java-elasticsearch/files/EsTemplateApplier.java +86 -0
- package/src/templates/db-java-elasticsearch/files/db-README.md +63 -0
- package/src/templates/db-java-elasticsearch/files/index-template-init.json +25 -0
- package/src/templates/db-java-elasticsearch/files/pom.xml +134 -0
- package/src/templates/db-java-elasticsearch/fragment.yaml +19 -0
- package/src/templates/db-java-flyway-mysql/files/V1__init.sql +44 -0
- package/src/templates/db-java-flyway-mysql/files/application.yaml +39 -0
- package/src/templates/db-java-flyway-mysql/files/db-README.md +102 -0
- package/src/templates/db-java-flyway-mysql/files/pom.xml +172 -0
- package/src/templates/db-java-flyway-mysql/fragment.yaml +19 -0
- package/src/templates/db-java-flyway-postgres/files/V1__init.sql +40 -0
- package/src/templates/db-java-flyway-postgres/files/application.yaml +37 -0
- package/src/templates/db-java-flyway-postgres/files/db-README.md +75 -0
- package/src/templates/db-java-flyway-postgres/files/pom.xml +166 -0
- package/src/templates/db-java-flyway-postgres/fragment.yaml +19 -0
- package/src/templates/db-node-elasticsearch/files/apply-templates.cjs +60 -0
- package/src/templates/db-node-elasticsearch/files/db-README.md +76 -0
- package/src/templates/db-node-elasticsearch/files/index-template-init.json +26 -0
- package/src/templates/db-node-elasticsearch/files/package.json +26 -0
- package/src/templates/db-node-elasticsearch/fragment.yaml +19 -0
- package/src/templates/db-node-knex-mysql/files/db-README.md +90 -0
- package/src/templates/db-node-knex-mysql/files/knexfile.cjs +72 -0
- package/src/templates/db-node-knex-mysql/files/migrations-init.cjs +42 -0
- package/src/templates/db-node-knex-mysql/files/package.json +31 -0
- package/src/templates/db-node-knex-mysql/files/seeds-dev-fixtures.cjs +38 -0
- package/src/templates/db-node-knex-mysql/files/seeds-prod-dictionaries.cjs +25 -0
- package/src/templates/db-node-knex-mysql/fragment.yaml +25 -0
- package/src/templates/db-node-knex-postgres/files/db-README.md +81 -0
- package/src/templates/db-node-knex-postgres/files/knexfile.cjs +67 -0
- package/src/templates/db-node-knex-postgres/files/migrations-init.cjs +42 -0
- package/src/templates/db-node-knex-postgres/files/package.json +31 -0
- package/src/templates/db-node-knex-postgres/files/seeds-dev-fixtures.cjs +36 -0
- package/src/templates/db-node-knex-postgres/files/seeds-prod-dictionaries.cjs +26 -0
- package/src/templates/db-node-knex-postgres/fragment.yaml +25 -0
- package/src/templates/db-python-alembic-mysql/files/0001_init.py +70 -0
- package/src/templates/db-python-alembic-mysql/files/alembic.ini +47 -0
- package/src/templates/db-python-alembic-mysql/files/db-README.md +87 -0
- package/src/templates/db-python-alembic-mysql/files/env.py +71 -0
- package/src/templates/db-python-alembic-mysql/files/pyproject.toml +52 -0
- package/src/templates/db-python-alembic-mysql/files/script.py.mako +26 -0
- package/src/templates/db-python-alembic-mysql/fragment.yaml +25 -0
- package/src/templates/db-python-alembic-postgres/files/0001_init.py +62 -0
- package/src/templates/db-python-alembic-postgres/files/alembic.ini +45 -0
- package/src/templates/db-python-alembic-postgres/files/db-README.md +70 -0
- package/src/templates/db-python-alembic-postgres/files/env.py +62 -0
- package/src/templates/db-python-alembic-postgres/files/pyproject.toml +52 -0
- package/src/templates/db-python-alembic-postgres/files/script.py.mako +25 -0
- package/src/templates/db-python-alembic-postgres/fragment.yaml +25 -0
- package/src/templates/db-python-elasticsearch/files/apply_templates.py +55 -0
- package/src/templates/db-python-elasticsearch/files/db-README.md +57 -0
- package/src/templates/db-python-elasticsearch/files/index-template-init.json +25 -0
- package/src/templates/db-python-elasticsearch/files/pyproject.toml +50 -0
- package/src/templates/db-python-elasticsearch/fragment.yaml +19 -0
- package/src/templates/docs-skeleton/files/governance-change-tiers.md +135 -0
- package/src/templates/docs-skeleton/files/governance-database.md +150 -0
- package/src/templates/docs-skeleton/fragment.yaml +6 -0
- package/src/templates/root-files/files/AGENTS.md +42 -7
- package/src/templates/server-node/files/_eslintrc.cjs +5 -1
- package/src/templates/server-python/files/README.md +75 -2
- package/src/templates/server-python/files/app-init.py +11 -1
- package/src/templates/server-python/files/config.py +40 -0
- package/src/templates/server-python/files/main.py +62 -0
- package/src/templates/server-python/files/pyproject.toml +14 -1
- package/src/templates/server-python/files/test_healthz.py +36 -0
- package/src/templates/server-python/fragment.yaml +10 -1
- package/src/templates/web-react/files/_eslintrc.cjs +5 -1
- package/src/templates/web-vue/files/_eslintrc.cjs +5 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent Elasticsearch index-template applier.
|
|
3
|
+
*
|
|
4
|
+
* 与 RDBMS 的迁移不同,ES 没有事务性 DDL;"migration" 在这里退化为
|
|
5
|
+
* "把 db/index-templates/*.json 全部 PUT 一次"。文件按字典序应用;
|
|
6
|
+
* ES 自身保证 PUT 同名 template 的 idempotency。
|
|
7
|
+
*
|
|
8
|
+
* 命名规则:<utc-timestamp>_<description>.json,其中 JSON 内容遵循
|
|
9
|
+
* Index Templates v2 schema(含 index_patterns / template / priority 字段)。
|
|
10
|
+
*
|
|
11
|
+
* 改动 tier(参见 docs/governance/change-tiers.md):
|
|
12
|
+
* - 新增 template 文件:Tier 3
|
|
13
|
+
* - 修改既有 template 的 mapping 字段:Tier 4(破坏性 - 可能 reindex)
|
|
14
|
+
* - 仅改 priority 或新增 alias:Tier 2
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const { Client } = require('@elastic/elasticsearch');
|
|
19
|
+
|
|
20
|
+
const TEMPLATE_DIR = path.join(__dirname, 'index-templates');
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const url = process.env.ELASTICSEARCH_URL || 'http://127.0.0.1:9200';
|
|
24
|
+
const username = process.env.ELASTICSEARCH_USERNAME;
|
|
25
|
+
const password = process.env.ELASTICSEARCH_PASSWORD;
|
|
26
|
+
|
|
27
|
+
const client = new Client({
|
|
28
|
+
node: url,
|
|
29
|
+
...(username && password ? { auth: { username, password } } : {}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 健康检查:CI 起服务容器时偶尔 race condition
|
|
33
|
+
await client.cluster.health({ wait_for_status: 'yellow', timeout: '30s' });
|
|
34
|
+
|
|
35
|
+
const files = fs
|
|
36
|
+
.readdirSync(TEMPLATE_DIR)
|
|
37
|
+
.filter((f) => f.endsWith('.json'))
|
|
38
|
+
.sort();
|
|
39
|
+
|
|
40
|
+
if (files.length === 0) {
|
|
41
|
+
console.log('no index templates found in', TEMPLATE_DIR);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const fullPath = path.join(TEMPLATE_DIR, file);
|
|
47
|
+
const body = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
48
|
+
// 模板名取文件名去后缀,避免依赖 JSON 内 name 字段
|
|
49
|
+
const name = path.basename(file, '.json');
|
|
50
|
+
await client.indices.putIndexTemplate({ name, ...body });
|
|
51
|
+
console.log(`✓ applied ${name}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`done; ${files.length} template(s) applied to ${url}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main().catch((err) => {
|
|
58
|
+
console.error('✗ failed:', err.message || err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# server/db/
|
|
2
|
+
|
|
3
|
+
Elasticsearch index-template 入口(Node 后端)。
|
|
4
|
+
|
|
5
|
+
> 跨项目的数据库归属规则见 [`docs/governance/database.md`](../../docs/governance/database.md)。
|
|
6
|
+
|
|
7
|
+
## "Migration" 的语义
|
|
8
|
+
|
|
9
|
+
**Elasticsearch 没有事务性 DDL,本目录里的 JSON 不是迁移文件。**
|
|
10
|
+
|
|
11
|
+
每个 `db/index-templates/*.json` 是一个 [Index Template v2](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html)。我们的 applier 脚本:
|
|
12
|
+
|
|
13
|
+
1. 按文件名字典序遍历 `index-templates/` 目录
|
|
14
|
+
2. 对每个文件 `PUT _index_template/<filename-without-ext>`
|
|
15
|
+
3. 由 ES 自身保证 PUT 同名模板的幂等性
|
|
16
|
+
|
|
17
|
+
意味着多次跑 `npm run es:apply` 不会"叠加"——它只是把当前代码声明的所有 template 同步到集群。
|
|
18
|
+
|
|
19
|
+
## 工具与版本
|
|
20
|
+
|
|
21
|
+
| 项 | 值 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Elasticsearch | 8.15+ |
|
|
24
|
+
| Node 客户端 | `@elastic/elasticsearch` `8.15.0` |
|
|
25
|
+
|
|
26
|
+
## 目录布局
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
server/
|
|
30
|
+
├── package.json # @elastic/elasticsearch 8.15.0
|
|
31
|
+
└── db/
|
|
32
|
+
├── README.md
|
|
33
|
+
├── apply-templates.cjs # 幂等 applier
|
|
34
|
+
└── index-templates/
|
|
35
|
+
└── <utc-ts>_<description>.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 文件命名规则
|
|
39
|
+
|
|
40
|
+
- `<utc-timestamp>_<description>.json`
|
|
41
|
+
- 时间戳仅用于排序(ES 不关心);让 PR review 一眼看到上线先后
|
|
42
|
+
- 文件名(去后缀)即 ES 中的 template 名
|
|
43
|
+
|
|
44
|
+
## 常用命令
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run es:apply
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
环境变量:`ELASTICSEARCH_URL`(默认 `http://127.0.0.1:9200`)、`ELASTICSEARCH_USERNAME` / `ELASTICSEARCH_PASSWORD`(可选)。
|
|
51
|
+
|
|
52
|
+
## 改动分级(参见 `docs/governance/change-tiers.md`)
|
|
53
|
+
|
|
54
|
+
| 改动 | Tier | 说明 |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| 新增 template 文件 | 3 | 写入 contracts CHANGELOG(事件契约依赖 template) |
|
|
57
|
+
| 修改既有 template 的 mapping 字段 | 4 | 破坏性:可能需要 reindex;写迁移预案 |
|
|
58
|
+
| 仅改 priority / 新增 alias / 改 settings.refresh_interval | 2 | 局部行为调整 |
|
|
59
|
+
| 删除 template | 4 | ADR 记录原因 |
|
|
60
|
+
|
|
61
|
+
## 与 `contracts/asyncapi/` 的同步
|
|
62
|
+
|
|
63
|
+
事件 ingest 的 schema 真值在 `contracts/asyncapi/asyncapi.yaml`;index-template 的 mapping 应当与之**对齐**:
|
|
64
|
+
|
|
65
|
+
- 字段名一致
|
|
66
|
+
- 类型映射一致(`date` ↔ AsyncAPI `format: date-time`)
|
|
67
|
+
|
|
68
|
+
当前是手动同步;自动派生留给后续 `contract-derived-mappings` 特性。
|
|
69
|
+
|
|
70
|
+
## CI 行为
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run es:apply
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
CI 在 service container 里起一个 ES 8 实例;脚本失败 = 任一 template 应用失败。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"index_patterns": ["events-*"],
|
|
3
|
+
"priority": 100,
|
|
4
|
+
"template": {
|
|
5
|
+
"settings": {
|
|
6
|
+
"number_of_shards": 1,
|
|
7
|
+
"number_of_replicas": 1,
|
|
8
|
+
"refresh_interval": "5s",
|
|
9
|
+
"index.lifecycle.name": "events-policy"
|
|
10
|
+
},
|
|
11
|
+
"mappings": {
|
|
12
|
+
"properties": {
|
|
13
|
+
"@timestamp": { "type": "date" },
|
|
14
|
+
"tenant_id": { "type": "keyword" },
|
|
15
|
+
"event_type": { "type": "keyword" },
|
|
16
|
+
"user_id": { "type": "keyword" },
|
|
17
|
+
"payload": { "type": "object", "dynamic": true },
|
|
18
|
+
"trace_id": { "type": "keyword" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"_meta": {
|
|
23
|
+
"description": "Sample event-stream template. Replace mappings with your domain fields.",
|
|
24
|
+
"managed_by": "<%- '@wneng/create-keel' %>"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= it.options.projectName %>-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node src/index.js",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "eslint src --ext .ts",
|
|
10
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
11
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
12
|
+
"test": "echo \"add tests with vitest or jest\" && exit 0",
|
|
13
|
+
"es:apply": "node db/apply-templates.cjs"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@elastic/elasticsearch": "8.15.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.14.10",
|
|
20
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
21
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
22
|
+
"eslint": "^8.57.0",
|
|
23
|
+
"prettier": "^3.3.3",
|
|
24
|
+
"typescript": "^5.5.4"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: db-node-elasticsearch
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
appliesWhen:
|
|
4
|
+
backend: node
|
|
5
|
+
database: elasticsearch
|
|
6
|
+
priority: 40
|
|
7
|
+
files:
|
|
8
|
+
- from: files/package.json
|
|
9
|
+
to: server/package.json
|
|
10
|
+
render: true
|
|
11
|
+
- from: files/apply-templates.cjs
|
|
12
|
+
to: server/db/apply-templates.cjs
|
|
13
|
+
render: false
|
|
14
|
+
- from: files/index-template-init.json
|
|
15
|
+
to: server/db/index-templates/20260101000000_init.json
|
|
16
|
+
render: false
|
|
17
|
+
- from: files/db-README.md
|
|
18
|
+
to: server/db/README.md
|
|
19
|
+
render: true
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# server/db/
|
|
2
|
+
|
|
3
|
+
数据库迁移、种子数据、本地数据库工具的入口。
|
|
4
|
+
|
|
5
|
+
> 跨项目的数据库归属规则(哪类 SQL 放哪个目录、与 `contracts/` 如何对齐)见 [`docs/governance/database.md`](../../docs/governance/database.md)。本文件只解释**本项目的工具与命令**。
|
|
6
|
+
|
|
7
|
+
## 工具与版本
|
|
8
|
+
|
|
9
|
+
| 项 | 值 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 数据库 | MySQL 8 / MariaDB 10.6+ |
|
|
12
|
+
| 迁移工具 | [Knex](https://knexjs.org) `3.1.0` |
|
|
13
|
+
| 驱动 | `mysql2` `3.11.3` |
|
|
14
|
+
|
|
15
|
+
工具版本在 `package.json` 钉死,并镜像到 `docs/03-工程规范与研发基础设施/tech-stack-server.md`。governance-lint 检测两边漂移并阻断 PR;升级时同 PR 改两个文件。
|
|
16
|
+
|
|
17
|
+
## 目录布局
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
server/
|
|
21
|
+
├── knexfile.cjs # Knex 入口配置,按环境变量切换 dev/prod/test
|
|
22
|
+
└── db/
|
|
23
|
+
├── README.md # 本文件
|
|
24
|
+
├── migrations/ # schema 迁移(DDL)
|
|
25
|
+
│ └── <ts>_<description>.cjs
|
|
26
|
+
└── seeds/
|
|
27
|
+
├── prod/ # 生产必装数据(字典 / 角色 / 错误码)
|
|
28
|
+
│ └── <ts>_seed_<topic>.cjs
|
|
29
|
+
└── dev/ # 仅本地 / 测试环境
|
|
30
|
+
└── <ts>_seed_<topic>.cjs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 常用命令
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 新建迁移文件
|
|
37
|
+
npm run db:make -- create_orders_table
|
|
38
|
+
|
|
39
|
+
# 升到最新
|
|
40
|
+
npm run db:migrate
|
|
41
|
+
|
|
42
|
+
# 回滚最近一组
|
|
43
|
+
npm run db:rollback
|
|
44
|
+
|
|
45
|
+
# 加载生产字典数据
|
|
46
|
+
npm run db:seed:prod
|
|
47
|
+
|
|
48
|
+
# 加载开发样例数据(不要在生产跑)
|
|
49
|
+
npm run db:seed:dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
环境变量见根 `.env.example`:`DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME`。
|
|
53
|
+
|
|
54
|
+
## prod vs dev seeds
|
|
55
|
+
|
|
56
|
+
| 维度 | `seeds/prod/` | `seeds/dev/` |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| 内容 | 字典数据、角色码表、错误码 lookup | 样例用户、demo 商品、调试 fixture |
|
|
59
|
+
| 与契约关系 | 与 `contracts/dictionaries/` 对齐 | 与契约无关 |
|
|
60
|
+
| CI 行为 | CI 在测试环境会跑 | CI 不跑 |
|
|
61
|
+
| 上线 | 部署流水线跑 | 不上线 |
|
|
62
|
+
| 改动 tier | 改 prod seed 至少 Tier 3(同步更新 `contracts/CHANGELOG.md`) | 改 dev seed 通常 Tier 1/2 |
|
|
63
|
+
|
|
64
|
+
## 与 `contracts/dictionaries/` 的同步
|
|
65
|
+
|
|
66
|
+
字典类生产种子(角色 / 错误码 / 状态机枚举)的真值在 `contracts/dictionaries/enums.yaml`。改字典的 PR 必须**同步**更新 `seeds/prod/` 下对应文件。
|
|
67
|
+
|
|
68
|
+
> 自动派生(contracts → seed migration)将由后续 `contract-derived-seeds` 特性实现;当前需要人工同步。
|
|
69
|
+
|
|
70
|
+
## 改动分级(参见 `docs/governance/change-tiers.md`)
|
|
71
|
+
|
|
72
|
+
| 改动 | Tier |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 新增迁移加表 / 加字段 | 3(contract 一致性 + CHANGELOG) |
|
|
75
|
+
| 修改既有迁移文件 | **不允许**;写新迁移代替 |
|
|
76
|
+
| 删字段 / 改类型(破坏性) | 4(ADR + 详细设计 + 迁移预案) |
|
|
77
|
+
| 改 dev seed | 1/2 |
|
|
78
|
+
| 改 prod seed(字典数据) | 3(同步 contracts/dictionaries/) |
|
|
79
|
+
| 升级 Knex / mysql2 | 2,但同步改 tech-stack-server.md |
|
|
80
|
+
|
|
81
|
+
## CI 行为
|
|
82
|
+
|
|
83
|
+
每个 PR 的 backend job 跑:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm run db:migrate
|
|
87
|
+
npm run db:rollback
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
只验证迁移**能跑通 + 能回滚**,不跑应用代码 / ORM 测试。完整 CI 矩阵见 `.github/workflows/ci.yml` 或 `.gitee/pipelines/ci.yml`。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knex configuration for <%= it.options.projectName %>-server (MySQL).
|
|
3
|
+
*
|
|
4
|
+
* Driver versions are pinned in package.json and mirrored in
|
|
5
|
+
* docs/03-工程规范与研发基础设施/tech-stack-server.md so governance-lint can
|
|
6
|
+
* detect drift. To upgrade Knex or mysql2, bump both files in the same PR.
|
|
7
|
+
*
|
|
8
|
+
* Connection comes from environment variables (.env.example documents
|
|
9
|
+
* the full list). Production deployments inject the same names via
|
|
10
|
+
* deploy/<target>/values.yaml or equivalent.
|
|
11
|
+
*/
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const base = {
|
|
15
|
+
client: 'mysql2',
|
|
16
|
+
migrations: {
|
|
17
|
+
directory: path.join(__dirname, 'db', 'migrations'),
|
|
18
|
+
extension: 'cjs',
|
|
19
|
+
tableName: 'knex_migrations',
|
|
20
|
+
},
|
|
21
|
+
seeds: {
|
|
22
|
+
// Knex's seed runner reads a single directory; we point it at prod
|
|
23
|
+
// by default and route dev seeds via `npm run db:seed:dev`.
|
|
24
|
+
directory: path.join(__dirname, 'db', 'seeds', 'prod'),
|
|
25
|
+
extension: 'cjs',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** @type {import('knex').Knex.Config} */
|
|
30
|
+
const development = {
|
|
31
|
+
...base,
|
|
32
|
+
connection: {
|
|
33
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
34
|
+
port: Number(process.env.DB_PORT || 3306),
|
|
35
|
+
user: process.env.DB_USER || 'root',
|
|
36
|
+
password: process.env.DB_PASSWORD || '',
|
|
37
|
+
database: process.env.DB_NAME || '<%= it.options.projectName.replace(/-/g, "_") %>_dev',
|
|
38
|
+
charset: 'utf8mb4',
|
|
39
|
+
},
|
|
40
|
+
pool: { min: 0, max: 10 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @type {import('knex').Knex.Config} */
|
|
44
|
+
const production = {
|
|
45
|
+
...base,
|
|
46
|
+
connection: {
|
|
47
|
+
host: process.env.DB_HOST,
|
|
48
|
+
port: Number(process.env.DB_PORT || 3306),
|
|
49
|
+
user: process.env.DB_USER,
|
|
50
|
+
password: process.env.DB_PASSWORD,
|
|
51
|
+
database: process.env.DB_NAME,
|
|
52
|
+
charset: 'utf8mb4',
|
|
53
|
+
},
|
|
54
|
+
pool: { min: 2, max: 20 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** @type {import('knex').Knex.Config} */
|
|
58
|
+
const test = {
|
|
59
|
+
...base,
|
|
60
|
+
connection: {
|
|
61
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
62
|
+
port: Number(process.env.DB_PORT || 3306),
|
|
63
|
+
user: process.env.DB_USER || 'root',
|
|
64
|
+
password: process.env.DB_PASSWORD || '',
|
|
65
|
+
database: process.env.DB_NAME || '<%= it.options.projectName.replace(/-/g, "_") %>_test',
|
|
66
|
+
charset: 'utf8mb4',
|
|
67
|
+
multipleStatements: true,
|
|
68
|
+
},
|
|
69
|
+
pool: { min: 0, max: 5 },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = { development, production, test };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 初始化迁移:创建 users 与 roles 两张样例表。
|
|
3
|
+
*
|
|
4
|
+
* 命名规则:<utc-timestamp>_<description>.cjs。新建迁移用 `npm run db:make -- <name>`。
|
|
5
|
+
* 修改既有迁移属于破坏性变更,应升 Tier 4(参见 docs/governance/change-tiers.md):
|
|
6
|
+
* 改字段类型、删字段、改约束都不允许直接覆盖已合入 main 的迁移;写一条新迁移。
|
|
7
|
+
*
|
|
8
|
+
* MySQL 注意:
|
|
9
|
+
* - 默认 charset 用 utf8mb4 避免 emoji 截断
|
|
10
|
+
* - 主键统一用 BIGINT UNSIGNED + AUTO_INCREMENT;如需分布式 ID 改用 UUID/雪花算法时一次性切换
|
|
11
|
+
* - 时间列统一用 TIMESTAMP(6) 保留毫秒精度
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
exports.up = async function up(knex) {
|
|
15
|
+
await knex.schema.createTable('roles', (t) => {
|
|
16
|
+
t.bigIncrements('id').unsigned().primary();
|
|
17
|
+
t.string('code', 64).notNullable().unique();
|
|
18
|
+
t.string('name', 128).notNullable();
|
|
19
|
+
t.text('description').nullable();
|
|
20
|
+
t.timestamp('created_at', { precision: 6 }).defaultTo(knex.fn.now(6));
|
|
21
|
+
t.timestamp('updated_at', { precision: 6 }).defaultTo(knex.fn.now(6));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await knex.schema.createTable('users', (t) => {
|
|
25
|
+
t.bigIncrements('id').unsigned().primary();
|
|
26
|
+
t.string('email', 255).notNullable().unique();
|
|
27
|
+
t.string('password_hash', 255).notNullable();
|
|
28
|
+
t.string('display_name', 128).nullable();
|
|
29
|
+
t.bigInteger('role_id').unsigned().notNullable().references('id').inTable('roles');
|
|
30
|
+
t.timestamp('created_at', { precision: 6 }).defaultTo(knex.fn.now(6));
|
|
31
|
+
t.timestamp('updated_at', { precision: 6 }).defaultTo(knex.fn.now(6));
|
|
32
|
+
t.timestamp('deleted_at', { precision: 6 }).nullable();
|
|
33
|
+
|
|
34
|
+
t.index(['role_id'], 'idx_users_role_id');
|
|
35
|
+
t.index(['deleted_at'], 'idx_users_deleted_at');
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
exports.down = async function down(knex) {
|
|
40
|
+
await knex.schema.dropTableIfExists('users');
|
|
41
|
+
await knex.schema.dropTableIfExists('roles');
|
|
42
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= it.options.projectName %>-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node src/index.js",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "eslint src --ext .ts",
|
|
10
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
11
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
12
|
+
"test": "echo \"add tests with vitest or jest\" && exit 0",
|
|
13
|
+
"db:migrate": "knex migrate:latest",
|
|
14
|
+
"db:rollback": "knex migrate:rollback",
|
|
15
|
+
"db:seed:prod": "knex seed:run --specific=db/seeds/prod",
|
|
16
|
+
"db:seed:dev": "knex seed:run --specific=db/seeds/dev",
|
|
17
|
+
"db:make": "knex migrate:make"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"knex": "3.1.0",
|
|
21
|
+
"mysql2": "3.11.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.14.10",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
26
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
27
|
+
"eslint": "^8.57.0",
|
|
28
|
+
"prettier": "^3.3.3",
|
|
29
|
+
"typescript": "^5.5.4"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 开发种子:仅用于本地 / 测试环境的样例数据。
|
|
3
|
+
*
|
|
4
|
+
* **绝不**让这里的数据进入生产库。CI 流水线只跑 prod 种子,dev 种子由
|
|
5
|
+
* 开发者按需 `npm run db:seed:dev` 手动加载。
|
|
6
|
+
*
|
|
7
|
+
* 包含真实邮箱、姓名等也放在这里——前提是它们都是构造数据,不是真实 PII。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
exports.seed = async function seed(knex) {
|
|
11
|
+
// 依赖 prod 种子先跑过,roles 表里至少有 'user' 与 'admin'
|
|
12
|
+
const roles = await knex('roles').whereIn('code', ['admin', 'user']).select('id', 'code');
|
|
13
|
+
const adminRoleId = roles.find((r) => r.code === 'admin')?.id;
|
|
14
|
+
const userRoleId = roles.find((r) => r.code === 'user')?.id;
|
|
15
|
+
|
|
16
|
+
if (adminRoleId === undefined || userRoleId === undefined) {
|
|
17
|
+
throw new Error('prod seeds must run first; roles "admin" / "user" missing');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 清空仅 dev fixture 的样例用户(避免重复跑导致约束冲突)
|
|
21
|
+
await knex('users').whereIn('email', ['admin@example.com', 'alice@example.com']).delete();
|
|
22
|
+
|
|
23
|
+
await knex('users').insert([
|
|
24
|
+
{
|
|
25
|
+
email: 'admin@example.com',
|
|
26
|
+
// bcrypt('admin123') 的占位 hash,仅供 dev 用
|
|
27
|
+
password_hash: '$2b$10$placeholder.dev.only.do.not.use.in.production.hash',
|
|
28
|
+
display_name: '系统管理员',
|
|
29
|
+
role_id: adminRoleId,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
email: 'alice@example.com',
|
|
33
|
+
password_hash: '$2b$10$placeholder.dev.only.do.not.use.in.production.hash',
|
|
34
|
+
display_name: 'Alice',
|
|
35
|
+
role_id: userRoleId,
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生产种子:字典数据。
|
|
3
|
+
*
|
|
4
|
+
* 与 contracts/dictionaries/ 对齐——这些 code/name 是 contract 派生而来的,
|
|
5
|
+
* 修改 contracts/dictionaries/enums.yaml 的同一 PR 中必须同步更新这里。
|
|
6
|
+
* (0.4.0 仍是手动同步;自动派生留给后续 contract-derived-seeds 特性)
|
|
7
|
+
*
|
|
8
|
+
* Idempotent:用 ON DUPLICATE KEY UPDATE 让多次执行结果一致。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
exports.seed = async function seed(knex) {
|
|
12
|
+
const roles = [
|
|
13
|
+
{ code: 'admin', name: '系统管理员', description: '可执行所有管理操作' },
|
|
14
|
+
{ code: 'user', name: '普通用户', description: '默认注册角色' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
for (const r of roles) {
|
|
18
|
+
await knex.raw(
|
|
19
|
+
`INSERT INTO roles (code, name, description)
|
|
20
|
+
VALUES (?, ?, ?)
|
|
21
|
+
ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description)`,
|
|
22
|
+
[r.code, r.name, r.description],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: db-node-knex-mysql
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
appliesWhen:
|
|
4
|
+
backend: node
|
|
5
|
+
database: mysql
|
|
6
|
+
priority: 40
|
|
7
|
+
files:
|
|
8
|
+
- from: files/package.json
|
|
9
|
+
to: server/package.json
|
|
10
|
+
render: true
|
|
11
|
+
- from: files/knexfile.cjs
|
|
12
|
+
to: server/knexfile.cjs
|
|
13
|
+
render: true
|
|
14
|
+
- from: files/migrations-init.cjs
|
|
15
|
+
to: server/db/migrations/20260101000000_init.cjs
|
|
16
|
+
render: false
|
|
17
|
+
- from: files/seeds-prod-dictionaries.cjs
|
|
18
|
+
to: server/db/seeds/prod/20260101000000_seed_dictionaries.cjs
|
|
19
|
+
render: false
|
|
20
|
+
- from: files/seeds-dev-fixtures.cjs
|
|
21
|
+
to: server/db/seeds/dev/20260101000000_seed_dev_fixtures.cjs
|
|
22
|
+
render: false
|
|
23
|
+
- from: files/db-README.md
|
|
24
|
+
to: server/db/README.md
|
|
25
|
+
render: true
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# server/db/
|
|
2
|
+
|
|
3
|
+
数据库迁移、种子数据、本地数据库工具的入口。
|
|
4
|
+
|
|
5
|
+
> 跨项目的数据库归属规则(哪类 SQL 放哪个目录、与 `contracts/` 如何对齐)见 [`docs/governance/database.md`](../../docs/governance/database.md)。本文件只解释**本项目的工具与命令**。
|
|
6
|
+
|
|
7
|
+
## 工具与版本
|
|
8
|
+
|
|
9
|
+
| 项 | 值 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 数据库 | PostgreSQL 14+ |
|
|
12
|
+
| 迁移工具 | [Knex](https://knexjs.org) `3.1.0` |
|
|
13
|
+
| 驱动 | `pg` `8.13.0` |
|
|
14
|
+
|
|
15
|
+
工具版本在 `package.json` 钉死,并镜像到 `docs/03-工程规范与研发基础设施/tech-stack-server.md`。governance-lint 检测两边漂移并阻断 PR;升级时同 PR 改两个文件。
|
|
16
|
+
|
|
17
|
+
## 目录布局
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
server/
|
|
21
|
+
├── knexfile.cjs # Knex 入口配置,按环境变量切换 dev/prod/test
|
|
22
|
+
└── db/
|
|
23
|
+
├── README.md # 本文件
|
|
24
|
+
├── migrations/ # schema 迁移(DDL)
|
|
25
|
+
│ └── <ts>_<description>.cjs
|
|
26
|
+
└── seeds/
|
|
27
|
+
├── prod/ # 生产必装数据(字典 / 角色 / 错误码)
|
|
28
|
+
│ └── <ts>_seed_<topic>.cjs
|
|
29
|
+
└── dev/ # 仅本地 / 测试环境
|
|
30
|
+
└── <ts>_seed_<topic>.cjs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 常用命令
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run db:make -- create_orders_table # 新建迁移文件
|
|
37
|
+
npm run db:migrate # 升到最新
|
|
38
|
+
npm run db:rollback # 回滚最近一组
|
|
39
|
+
npm run db:seed:prod # 加载生产字典数据
|
|
40
|
+
npm run db:seed:dev # 加载开发样例数据
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
环境变量见根 `.env.example`:`DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` / `DB_SSL`。
|
|
44
|
+
|
|
45
|
+
## prod vs dev seeds
|
|
46
|
+
|
|
47
|
+
| 维度 | `seeds/prod/` | `seeds/dev/` |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| 内容 | 字典数据、角色码表、错误码 lookup | 样例用户、demo 商品、调试 fixture |
|
|
50
|
+
| 与契约关系 | 与 `contracts/dictionaries/` 对齐 | 与契约无关 |
|
|
51
|
+
| CI 行为 | CI 在测试环境会跑 | CI 不跑 |
|
|
52
|
+
| 上线 | 部署流水线跑 | 不上线 |
|
|
53
|
+
| 改动 tier | 改 prod seed 至少 Tier 3(同步更新 `contracts/CHANGELOG.md`) | 改 dev seed 通常 Tier 1/2 |
|
|
54
|
+
|
|
55
|
+
## 与 `contracts/dictionaries/` 的同步
|
|
56
|
+
|
|
57
|
+
字典类生产种子(角色 / 错误码 / 状态机枚举)的真值在 `contracts/dictionaries/enums.yaml`。改字典的 PR 必须**同步**更新 `seeds/prod/` 下对应文件。
|
|
58
|
+
|
|
59
|
+
> 自动派生(contracts → seed migration)将由后续 `contract-derived-seeds` 特性实现;当前需要人工同步。
|
|
60
|
+
|
|
61
|
+
## 改动分级(参见 `docs/governance/change-tiers.md`)
|
|
62
|
+
|
|
63
|
+
| 改动 | Tier |
|
|
64
|
+
|---|---|
|
|
65
|
+
| 新增迁移加表 / 加字段 | 3(contract 一致性 + CHANGELOG) |
|
|
66
|
+
| 修改既有迁移文件 | **不允许**;写新迁移代替 |
|
|
67
|
+
| 删字段 / 改类型(破坏性) | 4(ADR + 详细设计 + 迁移预案) |
|
|
68
|
+
| 改 dev seed | 1/2 |
|
|
69
|
+
| 改 prod seed(字典数据) | 3(同步 contracts/dictionaries/) |
|
|
70
|
+
| 升级 Knex / pg | 2,但同步改 tech-stack-server.md |
|
|
71
|
+
|
|
72
|
+
## CI 行为
|
|
73
|
+
|
|
74
|
+
每个 PR 的 backend job 跑:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run db:migrate
|
|
78
|
+
npm run db:rollback
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
只验证迁移**能跑通 + 能回滚**,不跑应用代码 / ORM 测试。完整 CI 矩阵见 `.github/workflows/ci.yml` 或 `.gitee/pipelines/ci.yml`。
|