@kne/fastify-app-manager 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -263,7 +263,7 @@ await fastify.listen({ port: 3000 });
263
263
  |--------|------|------|--------|------|
264
264
  | name | string | 是 | - | slug:`^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$` |
265
265
  | label | string | 是 | - | 展示名 |
266
- | domain | string | 否 | - | 绑定 Host(唯一);冲突返回 409 |
266
+ | domain | string / null | 否 | - | 绑定 Host(唯一);冲突返回 409;空值存为 `null` |
267
267
  | icon | string | 否 | - | 图标 |
268
268
  | description | string | 否 | - | 描述 |
269
269
  | category | object / string | 否 | - | 应用分类(应用中心分组用),写入 `options.category`;推荐传 fastify-group 分组对象,对象只保留 `{ code, name }`,字符串原样保存 |
@@ -282,7 +282,7 @@ await fastify.listen({ port: 3000 });
282
282
  | 参数名 | 类型 | 必填 | 说明 |
283
283
  |--------|------|------|------|
284
284
  | name | string | 是 | 应用 slug |
285
- | label / domain / icon / description / env / pm2Config / options | - | 否 | 传入的字段才更新;`env` 按 patch 语义合并 |
285
+ | label / domain / icon / description / env / pm2Config / options | - | 否 | 传入的字段才更新;`env` 按 patch 语义合并;`domain` 传 `null` / `''` 解绑域名(存为 `null`) |
286
286
  | category | object / string / null | 否 | 合并写入 `options.category`(对象只保留 `{ code, name }`),不影响 `options` 其它键;传空值清除分类 |
287
287
  | isPublic | boolean | 否 | 合并写入 `options.isPublic`,不影响 `options` 其它键 |
288
288
  | entries | array / null | 否 | 整体替换 `options.entries`(规则同创建),不影响 `options` 其它键;传 `[]` / `null` 清空入口 |
@@ -428,7 +428,7 @@ await fastify.listen({ port: 3000 });
428
428
 
429
429
  ##### POST `{prefix}/app/deploy`
430
430
 
431
- 选择版本部署。版本目录不在磁盘时先经 `resolveVersionFile` 取回 zip 解压并 `npm install`(耗时,取不回返回 400 且不改应用状态),再以数据库中的 SQL 覆盖目录内 `.sql`;接着按版本的 `env.schema.json` 校验最终环境变量(含注入的变量),版本没有 schema 或变量不合规时返回 400,且不切换版本、不执行迁移、不启动进程;随后立即返回 `deploying`,后台做健康检查。进程启动后删除该应用其它有 `fileId` 的版本目录(只保留当前版本)。
431
+ 选择版本部署。同一应用同时只能有一个部署:已有部署未结束(含就绪检查阶段)时返回 409,不影响进行中的部署。版本目录不在磁盘时先经 `resolveVersionFile` 取回 zip 解压并 `npm install`(耗时,取不回返回 400 且不改应用状态),再以数据库中的 SQL 覆盖目录内 `.sql`;接着按版本的 `env.schema.json` 校验最终环境变量(含注入的变量),版本没有 schema 或变量不合规时返回 400,且不切换版本、不执行迁移、不启动进程;随后立即返回 `deploying`,后台做健康检查。进程启动后删除该应用其它有 `fileId` 的版本目录(只保留当前版本)。
432
432
 
433
433
  | 参数名 | 类型 | 必填 | 默认值 | 说明 |
434
434
  |--------|------|------|--------|------|
@@ -757,7 +757,7 @@ data: {"name":"demo","ts":1791456608533,...}
757
757
  | `pruneAllVersionArtifacts()` | 对所有应用执行 `pruneVersionArtifacts` |
758
758
  | `restoreMissingArtifacts()` | 当前版本目录(`artifactPath/server`)缺失时,按版本 `fileId` 经 `resolveVersionFile` 取回 zip 重建并恢复 `current` 链接(不跑 SQL 迁移);返回 `[{ name, version, action: 'restored' \| 'skipped' \| 'failed', reason? }]`,无 `fileId` 的记 `skipped` |
759
759
  | `listVersions({ name, perPage, currentPage })` | 版本列表 |
760
- | `deploy({ name, versionId, version, runMigration, migrations?, progressId? })` | 部署;版本目录缺失时先从宿主文件重建,启动后清理其它版本目录;立即 `deploying`,后台 `syncRealStatus`;各阶段进度写入 `getDeployProgress()` |
760
+ | `deploy({ name, versionId, version, runMigration, migrations?, progressId? })` | 部署;同一应用已有部署未结束时 409;版本目录缺失时先从宿主文件重建,启动后清理其它版本目录;立即 `deploying`,后台 `syncRealStatus`;各阶段进度写入 `getDeployProgress()` |
761
761
  | `getDeployProgress()` | 部署进度中心:`emitter`(事件名 `deploy:{name}`、日志 `deploy-log:{name}`)、`snapshot(name)` 返回最近一次部署 `{ progressId, status, events, ... }`、`active(name)` 进行中的部署 |
762
762
  | `deployList({ name, perPage?, currentPage? })` | 同 `GET .../app/deploy/list` |
763
763
  | `deployDetail({ name, progressId?, logLines? })` | 同 `GET .../app/deploy/detail` |
@@ -23,7 +23,7 @@ module.exports = fp(async (fastify, options) => {
23
23
  properties: {
24
24
  name: { type: 'string' },
25
25
  label: { type: 'string' },
26
- domain: { type: 'string' },
26
+ domain: { type: ['string', 'null'] },
27
27
  icon: { type: 'string' },
28
28
  description: { type: 'string' },
29
29
  category: { type: ['object', 'string', 'null'] },
@@ -54,7 +54,7 @@ module.exports = fp(async (fastify, options) => {
54
54
  properties: {
55
55
  name: { type: 'string' },
56
56
  label: { type: 'string' },
57
- domain: { type: 'string' },
57
+ domain: { type: ['string', 'null'] },
58
58
  icon: { type: 'string' },
59
59
  description: { type: 'string' },
60
60
  category: { type: ['object', 'string', 'null'] },
@@ -181,6 +181,10 @@ module.exports = fp(async (fastify, options) => {
181
181
  }
182
182
  patch[k] = v;
183
183
  }
184
+ // 空域名存 null:domain 有唯一约束,多个应用存 '' 会冲突
185
+ if (patch.domain !== undefined) {
186
+ patch.domain = patch.domain || null;
187
+ }
184
188
  if (patch.domain) {
185
189
  const domainTaken = await models.app.findOne({ where: { domain: patch.domain } });
186
190
  if (domainTaken && domainTaken.name !== name) {
@@ -1089,6 +1093,11 @@ module.exports = fp(async (fastify, options) => {
1089
1093
  httpError(404, 'version not found');
1090
1094
  }
1091
1095
 
1096
+ // 判断与登记之间不能有 await,否则并发请求会同时通过
1097
+ const running = deployProgress.active(app.name);
1098
+ if (running) {
1099
+ httpError(409, `app ${app.name} is deploying version ${running.version}`);
1100
+ }
1092
1101
  const progress = deployProgress.begin(app.name, progressId || crypto.randomUUID(), { versionId: String(ver.id), version: ver.version });
1093
1102
  let step = 'package';
1094
1103
  progress.report(step, 'running');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kne/fastify-app-manager",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Fastify 单机应用市场插件:版本上传、PM2 托管、路径/域名反代与日志 SSE",
5
5
  "main": "index.js",
6
6
  "scripts": {