@kne/fastify-app-manager 0.1.9 → 0.1.11
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 +92 -21
- package/index.js +8 -0
- package/libs/controllers/app.js +105 -16
- package/libs/controllers/gateway.js +5 -2
- package/libs/models/appVersion.js +10 -0
- package/libs/services/app.js +324 -32
- package/libs/services/bootstrap.js +68 -5
- package/libs/services/dbops.js +2 -10
- package/libs/services/migration.js +34 -46
- package/libs/utils/dbIdentity.js +16 -7
- package/libs/utils/deployProgress.js +37 -0
- package/libs/utils/env.js +30 -11
- package/libs/utils/pathRewrite.js +28 -3
- package/libs/utils/version.js +7 -2
- package/libs/utils/versionSql.js +33 -0
- package/package.json +1 -1
package/libs/services/dbops.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const AdmZip = require('adm-zip');
|
|
5
5
|
const { createSequelizeFromEnv, SQL_MIGRATIONS_TABLE } = require('../utils/migrate');
|
|
6
6
|
const { resolveAppDbEnv, hasAppDbConfig } = require('../utils/env');
|
|
7
|
-
const { resolveDbScope, normalizeOwnedTables, isSystemTable, resolveTablePrefix } = require('../utils/dbIdentity');
|
|
7
|
+
const { resolveDbScope, normalizeOwnedTables, isSystemTable, resolveTablePrefix, filterOwnedTables } = require('../utils/dbIdentity');
|
|
8
8
|
const { assertReadOnlyQuerySql } = require('../utils/sqlQueryGuard');
|
|
9
9
|
|
|
10
10
|
module.exports = fp(async (fastify, options) => {
|
|
@@ -83,15 +83,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
83
83
|
return { table, columns, primaryKey };
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
const getOwnedTables = (app, allTables) => {
|
|
87
|
-
const scope = resolveDbScope(app.env || {}, app.options || {});
|
|
88
|
-
const existing = new Set(allTables);
|
|
89
|
-
if (scope === 'dedicated') {
|
|
90
|
-
return allTables.filter(t => !isSystemTable(t));
|
|
91
|
-
}
|
|
92
|
-
const owned = normalizeOwnedTables(app.options?.ownedTables);
|
|
93
|
-
return owned.filter(t => existing.has(t));
|
|
94
|
-
};
|
|
86
|
+
const getOwnedTables = (app, allTables) => filterOwnedTables({ appEnv: app.env || {}, ownedTables: app.options?.ownedTables, allTables });
|
|
95
87
|
|
|
96
88
|
const assertTableOwned = (app, allTables, table) => {
|
|
97
89
|
const owned = getOwnedTables(app, allTables);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
|
-
const { listSqlFiles } = require('../utils/validatePackage');
|
|
5
2
|
const { isValidSqlFileName, readExecutedMigrations, recordMigration, removeMigrationRecord } = require('../utils/migrate');
|
|
6
3
|
|
|
7
4
|
module.exports = fp(async (fastify, options) => {
|
|
8
5
|
const { models } = fastify[options.name];
|
|
9
6
|
const getDbops = () => fastify[options.name].services.dbops;
|
|
7
|
+
const getAppService = () => fastify[options.name].services.app;
|
|
10
8
|
|
|
11
9
|
const httpError = (statusCode, message) => {
|
|
12
10
|
if (fastify.httpErrors) {
|
|
@@ -27,8 +25,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
27
25
|
if (!ver || ver.appName !== name) {
|
|
28
26
|
httpError(404, 'version not found');
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
return { app, ver, sqlDir };
|
|
28
|
+
return { app, ver };
|
|
32
29
|
};
|
|
33
30
|
|
|
34
31
|
const assertFileName = file => {
|
|
@@ -37,25 +34,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
37
34
|
}
|
|
38
35
|
};
|
|
39
36
|
|
|
40
|
-
const
|
|
37
|
+
const getFiles = ver => getAppService().getMigrationFiles(ver);
|
|
38
|
+
|
|
39
|
+
const findFile = async (ver, file) => {
|
|
41
40
|
assertFileName(file);
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
41
|
+
const item = (await getFiles(ver)).find(f => f.name === file);
|
|
42
|
+
if (!item) {
|
|
44
43
|
httpError(404, `sql file ${file} not found`);
|
|
45
44
|
}
|
|
46
|
-
return
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const syncHasMigration = async (ver, sqlDir) => {
|
|
50
|
-
const hasMigration = (await listSqlFiles(sqlDir)).length > 0;
|
|
51
|
-
if (ver.hasMigration !== hasMigration) {
|
|
52
|
-
await ver.update({ hasMigration });
|
|
53
|
-
}
|
|
45
|
+
return item;
|
|
54
46
|
};
|
|
55
47
|
|
|
56
48
|
const list = async ({ name, versionId }) => {
|
|
57
|
-
const { app, ver
|
|
58
|
-
const files = await
|
|
49
|
+
const { app, ver } = await resolveVersion({ name, versionId });
|
|
50
|
+
const files = await getFiles(ver);
|
|
59
51
|
let executedMap = new Map();
|
|
60
52
|
let dbError = null;
|
|
61
53
|
try {
|
|
@@ -63,18 +55,13 @@ module.exports = fp(async (fastify, options) => {
|
|
|
63
55
|
} catch (e) {
|
|
64
56
|
dbError = e.message;
|
|
65
57
|
}
|
|
66
|
-
const pageData =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
executed: dbError ? null : executedMap.has(file),
|
|
74
|
-
executedAt: executedMap.get(file) || null
|
|
75
|
-
};
|
|
76
|
-
})
|
|
77
|
-
);
|
|
58
|
+
const pageData = files.map(item => ({
|
|
59
|
+
name: item.name,
|
|
60
|
+
size: Buffer.byteLength(item.content || '', 'utf8'),
|
|
61
|
+
updatedAt: item.updatedAt || null,
|
|
62
|
+
executed: dbError ? null : executedMap.has(item.name),
|
|
63
|
+
executedAt: executedMap.get(item.name) || null
|
|
64
|
+
}));
|
|
78
65
|
return {
|
|
79
66
|
versionId: String(ver.id),
|
|
80
67
|
version: ver.version,
|
|
@@ -87,32 +74,33 @@ module.exports = fp(async (fastify, options) => {
|
|
|
87
74
|
};
|
|
88
75
|
|
|
89
76
|
const content = async ({ name, versionId, file }) => {
|
|
90
|
-
const {
|
|
91
|
-
const
|
|
92
|
-
return { name: file, content:
|
|
77
|
+
const { ver } = await resolveVersion({ name, versionId });
|
|
78
|
+
const item = await findFile(ver, file);
|
|
79
|
+
return { name: file, content: item.content || '' };
|
|
93
80
|
};
|
|
94
81
|
|
|
95
82
|
const save = async ({ name, versionId, file, content: sql }) => {
|
|
96
83
|
assertFileName(file);
|
|
97
|
-
const { ver
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
await
|
|
84
|
+
const { ver } = await resolveVersion({ name, versionId });
|
|
85
|
+
const files = (await getFiles(ver)).filter(f => f.name !== file);
|
|
86
|
+
files.push({ name: file, content: sql || '', updatedAt: new Date().toISOString() });
|
|
87
|
+
await getAppService().saveMigrationFiles(ver, files);
|
|
101
88
|
return { name: file };
|
|
102
89
|
};
|
|
103
90
|
|
|
104
91
|
const remove = async ({ name, versionId, file }) => {
|
|
105
|
-
const { ver
|
|
106
|
-
|
|
107
|
-
await
|
|
108
|
-
|
|
92
|
+
const { ver } = await resolveVersion({ name, versionId });
|
|
93
|
+
await findFile(ver, file);
|
|
94
|
+
await getAppService().saveMigrationFiles(
|
|
95
|
+
ver,
|
|
96
|
+
(await getFiles(ver)).filter(f => f.name !== file)
|
|
97
|
+
);
|
|
109
98
|
return { name: file };
|
|
110
99
|
};
|
|
111
100
|
|
|
112
101
|
const execute = async ({ name, versionId, file }) => {
|
|
113
|
-
const { app,
|
|
114
|
-
const
|
|
115
|
-
const sql = await fs.readFile(filePath, 'utf8');
|
|
102
|
+
const { app, ver } = await resolveVersion({ name, versionId });
|
|
103
|
+
const { content: sql } = await findFile(ver, file);
|
|
116
104
|
const dbops = getDbops();
|
|
117
105
|
let beforeTables = [];
|
|
118
106
|
let afterTables = [];
|
|
@@ -131,8 +119,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
131
119
|
};
|
|
132
120
|
|
|
133
121
|
const mark = async ({ name, versionId, file }) => {
|
|
134
|
-
const { app,
|
|
135
|
-
await
|
|
122
|
+
const { app, ver } = await resolveVersion({ name, versionId });
|
|
123
|
+
await findFile(ver, file);
|
|
136
124
|
await getDbops().withSequelize(app, sequelize => recordMigration(sequelize, file));
|
|
137
125
|
return { name: file, executed: true };
|
|
138
126
|
};
|
package/libs/utils/dbIdentity.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const {
|
|
2
|
+
const { isAppDbEnabled } = require('./env');
|
|
3
3
|
|
|
4
4
|
const SYSTEM_TABLES = new Set(['_fs_sql_migrations']);
|
|
5
5
|
|
|
@@ -69,12 +69,8 @@ const assertDefaultAppDbSeparated = (defaultAppDb, hostSequelize) => {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return options.dbScope;
|
|
75
|
-
}
|
|
76
|
-
return hasAppDbConfig(appEnv) ? 'dedicated' : 'shared';
|
|
77
|
-
};
|
|
72
|
+
// 与 startProcess 注入的连接一致:只看 env(独立库配置 + DB_DEDICATED 开关)。options.dbScope 只是认领表时写入的缓存,env 变化后会过期
|
|
73
|
+
const resolveDbScope = (appEnv = {}) => (isAppDbEnabled(appEnv) ? 'dedicated' : 'shared');
|
|
78
74
|
|
|
79
75
|
const normalizeOwnedTables = tables => [...new Set((Array.isArray(tables) ? tables : []).map(t => String(t || '').trim()).filter(Boolean))];
|
|
80
76
|
|
|
@@ -104,8 +100,21 @@ const resolveTablePrefix = (appName, appEnv = {}) => {
|
|
|
104
100
|
return buildTablePrefix(appName);
|
|
105
101
|
};
|
|
106
102
|
|
|
103
|
+
/**
|
|
104
|
+
* 共享库:只认登记过的表;独立库:库内业务表全归本应用,但显式配了 DB_TABLE_PREFIX 说明该库与其它系统共用,只认本前缀的表
|
|
105
|
+
*/
|
|
106
|
+
const filterOwnedTables = ({ appEnv = {}, ownedTables, allTables = [] }) => {
|
|
107
|
+
if (resolveDbScope(appEnv) === 'dedicated') {
|
|
108
|
+
const prefix = String(appEnv.DB_TABLE_PREFIX || '').trim();
|
|
109
|
+
return allTables.filter(t => !isSystemTable(t) && (!prefix || String(t).startsWith(prefix)));
|
|
110
|
+
}
|
|
111
|
+
const existing = new Set(allTables);
|
|
112
|
+
return normalizeOwnedTables(ownedTables).filter(t => existing.has(t));
|
|
113
|
+
};
|
|
114
|
+
|
|
107
115
|
module.exports = {
|
|
108
116
|
SYSTEM_TABLES,
|
|
117
|
+
filterOwnedTables,
|
|
109
118
|
fingerprintDbConfig,
|
|
110
119
|
fingerprintFromEnv,
|
|
111
120
|
assertDefaultAppDbSeparated,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { EventEmitter } = require('node:events');
|
|
2
|
+
|
|
3
|
+
/** 部署阶段,按顺序推进:准备部署包 → SQL 迁移 → 启动进程 → 等待就绪 */
|
|
4
|
+
const DEPLOY_STEPS = ['package', 'migrate', 'start', 'ready'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 每个应用只保留最近一次部署的进度事件,供 SSE 订阅与断线重连回放。
|
|
8
|
+
* 事件:{ progressId, step, status: 'running' | 'done' | 'skipped' | 'error', detail?, message?, at }
|
|
9
|
+
*/
|
|
10
|
+
const createDeployProgress = () => {
|
|
11
|
+
const emitter = new EventEmitter();
|
|
12
|
+
emitter.setMaxListeners(0);
|
|
13
|
+
const latest = new Map();
|
|
14
|
+
|
|
15
|
+
const begin = (name, progressId) => {
|
|
16
|
+
const state = { progressId, events: [] };
|
|
17
|
+
latest.set(name, state);
|
|
18
|
+
const report = (step, status, { detail = null, message = null } = {}) => {
|
|
19
|
+
const event = { progressId, step, status, detail, message, at: new Date().toISOString() };
|
|
20
|
+
state.events.push(event);
|
|
21
|
+
emitter.emit(`deploy:${name}`, event);
|
|
22
|
+
return event;
|
|
23
|
+
};
|
|
24
|
+
return { progressId, report };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const snapshot = name => latest.get(name) || null;
|
|
28
|
+
|
|
29
|
+
const clear = name => latest.delete(name);
|
|
30
|
+
|
|
31
|
+
return { emitter, begin, snapshot, clear };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
DEPLOY_STEPS,
|
|
36
|
+
createDeployProgress
|
|
37
|
+
};
|
package/libs/utils/env.js
CHANGED
|
@@ -23,6 +23,22 @@ const hasAppDbConfig = (appEnv = {}) => {
|
|
|
23
23
|
return !!appEnv.DB_DATABASE;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const DB_DEDICATED_KEY = 'DB_DEDICATED';
|
|
27
|
+
const DB_CONNECTION_KEYS = ['DB_DIALECT', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'DB_STORAGE'];
|
|
28
|
+
|
|
29
|
+
// 配好独立库后默认启用;DB_DEDICATED=false 时保留配置但回落共享库
|
|
30
|
+
const isAppDbEnabled = (appEnv = {}) =>
|
|
31
|
+
hasAppDbConfig(appEnv) &&
|
|
32
|
+
String(appEnv[DB_DEDICATED_KEY] ?? '')
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase() !== 'false';
|
|
35
|
+
|
|
36
|
+
const omitDbConnection = (appEnv = {}) => {
|
|
37
|
+
const next = Object.assign({}, appEnv);
|
|
38
|
+
DB_CONNECTION_KEYS.forEach(key => delete next[key]);
|
|
39
|
+
return next;
|
|
40
|
+
};
|
|
41
|
+
|
|
26
42
|
const buildDefaultAppDbEnv = (defaultAppDb = {}) => {
|
|
27
43
|
const dialect = defaultAppDb.dialect || 'sqlite';
|
|
28
44
|
const env = { DB_DIALECT: dialect };
|
|
@@ -59,10 +75,11 @@ const buildDefaultAppDbEnv = (defaultAppDb = {}) => {
|
|
|
59
75
|
*/
|
|
60
76
|
const resolveAppDbEnv = ({ passthroughEnvKeys = [], appEnv = {}, defaultAppDb = null, port = null, includePort = false, appName = null, injectTablePrefix = true } = {}) => {
|
|
61
77
|
const base = Object.assign({}, pickHostEnv(passthroughEnvKeys));
|
|
62
|
-
|
|
78
|
+
const useAppDb = isAppDbEnabled(appEnv || {});
|
|
79
|
+
if (defaultAppDb && !useAppDb) {
|
|
63
80
|
Object.assign(base, buildDefaultAppDbEnv(defaultAppDb));
|
|
64
81
|
}
|
|
65
|
-
Object.assign(base, appEnv || {});
|
|
82
|
+
Object.assign(base, useAppDb ? appEnv || {} : omitDbConnection(appEnv || {}));
|
|
66
83
|
if (includePort && port != null) {
|
|
67
84
|
base.PORT = String(port);
|
|
68
85
|
}
|
|
@@ -105,17 +122,17 @@ const isSecretKey = (key, pattern, secretKeys = []) => {
|
|
|
105
122
|
return isPatternSecretKey(key, pattern);
|
|
106
123
|
};
|
|
107
124
|
|
|
108
|
-
// 由系统写入维护:不在接口中返回,也不接受外部新增 / 修改 /
|
|
125
|
+
// 由系统写入维护:不在接口中返回,也不接受外部新增 / 修改 / 删除;宿主可通过 systemEnvKeys 选项追加
|
|
109
126
|
const SYSTEM_ENV_KEYS = ['DB_TABLE_PREFIX'];
|
|
110
127
|
|
|
111
|
-
const isSystemEnvKey = key => SYSTEM_ENV_KEYS.includes(key);
|
|
128
|
+
const isSystemEnvKey = (key, systemEnvKeys = []) => SYSTEM_ENV_KEYS.includes(key) || (systemEnvKeys || []).includes(key);
|
|
112
129
|
|
|
113
|
-
const omitSystemEnv = (appEnv = {}) => Object.fromEntries(Object.entries(appEnv || {}).filter(([key]) => !isSystemEnvKey(key)));
|
|
130
|
+
const omitSystemEnv = (appEnv = {}, systemEnvKeys = []) => Object.fromEntries(Object.entries(appEnv || {}).filter(([key]) => !isSystemEnvKey(key, systemEnvKeys)));
|
|
114
131
|
|
|
115
|
-
const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
|
|
132
|
+
const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
|
|
116
133
|
const masked = {};
|
|
117
134
|
const explicit = normalizeSecretKeys(secretEnvKeys);
|
|
118
|
-
for (const [key, value] of Object.entries(omitSystemEnv(appEnv))) {
|
|
135
|
+
for (const [key, value] of Object.entries(omitSystemEnv(appEnv, systemEnvKeys))) {
|
|
119
136
|
if (isSecretKey(key, secretEnvKeyPattern, explicit)) {
|
|
120
137
|
masked[key] = value == null || value === '' ? null : SECRET_MASK;
|
|
121
138
|
} else {
|
|
@@ -131,11 +148,11 @@ const maskEnvForResponse = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys =
|
|
|
131
148
|
* SECRET_MASK => keep existing (when key is secret by pattern or explicit list)
|
|
132
149
|
* other => set
|
|
133
150
|
*/
|
|
134
|
-
const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
|
|
151
|
+
const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
|
|
135
152
|
const next = Object.assign({}, current);
|
|
136
153
|
const explicit = normalizeSecretKeys(secretEnvKeys);
|
|
137
154
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
138
|
-
if (isSystemEnvKey(key)) {
|
|
155
|
+
if (isSystemEnvKey(key, systemEnvKeys)) {
|
|
139
156
|
continue;
|
|
140
157
|
}
|
|
141
158
|
if (value === null) {
|
|
@@ -153,10 +170,10 @@ const applyEnvPatch = (current = {}, patch = {}, { secretEnvKeyPattern, secretEn
|
|
|
153
170
|
/**
|
|
154
171
|
* 响应中的密钥键列表 = 显式列表 ∪ 当前 env 中匹配 pattern 的键
|
|
155
172
|
*/
|
|
156
|
-
const collectSecretKeys = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [] } = {}) => {
|
|
173
|
+
const collectSecretKeys = (appEnv = {}, { secretEnvKeyPattern, secretEnvKeys = [], systemEnvKeys = [] } = {}) => {
|
|
157
174
|
const explicit = normalizeSecretKeys(secretEnvKeys);
|
|
158
175
|
const fromPattern = Object.keys(appEnv || {}).filter(key => isPatternSecretKey(key, secretEnvKeyPattern));
|
|
159
|
-
return normalizeSecretKeys([...explicit, ...fromPattern]).filter(key => !isSystemEnvKey(key));
|
|
176
|
+
return normalizeSecretKeys([...explicit, ...fromPattern]).filter(key => !isSystemEnvKey(key, systemEnvKeys));
|
|
160
177
|
};
|
|
161
178
|
|
|
162
179
|
const PM2_CONFIG_KEYS = ['exec_mode', 'instances', 'autorestart', 'max_memory_restart', 'max_restarts', 'min_uptime', 'kill_timeout', 'merge_logs'];
|
|
@@ -176,6 +193,8 @@ module.exports = {
|
|
|
176
193
|
normalizeSecretKeys,
|
|
177
194
|
pickHostEnv,
|
|
178
195
|
hasAppDbConfig,
|
|
196
|
+
DB_DEDICATED_KEY,
|
|
197
|
+
isAppDbEnabled,
|
|
179
198
|
buildDefaultAppDbEnv,
|
|
180
199
|
resolveAppDbEnv,
|
|
181
200
|
mergeEnv,
|
|
@@ -27,9 +27,11 @@ const rewriteSetCookiePath = (setCookie, mountPrefix) => {
|
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const TEXTUAL_TYPES = /text\/|
|
|
30
|
+
const TEXTUAL_TYPES = /text\/|json|xml|svg/i;
|
|
31
|
+
// 前端脚本已按注入的 runtimePublicUrl / runtimeApiUrl 自行拼前缀(如 axios baseURL + '/api/...'),再改写会得到 /app/x/app/x/api
|
|
32
|
+
const SCRIPT_TYPES = /javascript|ecmascript/i;
|
|
31
33
|
|
|
32
|
-
const shouldRewriteBody = contentType => TEXTUAL_TYPES.test(contentType || '');
|
|
34
|
+
const shouldRewriteBody = contentType => TEXTUAL_TYPES.test(contentType || '') && !SCRIPT_TYPES.test(contentType || '');
|
|
33
35
|
|
|
34
36
|
const rewriteBody = (body, mountPrefix, prefixes = ['/static', '/api', '/account']) => {
|
|
35
37
|
if (!body || !mountPrefix) {
|
|
@@ -47,9 +49,32 @@ const rewriteBody = (body, mountPrefix, prefixes = ['/static', '/api', '/account
|
|
|
47
49
|
return text;
|
|
48
50
|
};
|
|
49
51
|
|
|
52
|
+
// 路径模式响应体经网关改写,ETag 带上改写规则版本,与子应用原始文件及旧规则下的缓存区分;改写规则变化时递增
|
|
53
|
+
const ETAG_SUFFIX = '-gw2';
|
|
54
|
+
|
|
55
|
+
const tagEtag = etag => (typeof etag === 'string' && etag.endsWith('"') ? `${etag.slice(0, -1)}${ETAG_SUFFIX}"` : etag);
|
|
56
|
+
|
|
57
|
+
// 只有本网关签发的 ETag 才还原后交给子应用协商缓存;其它(旧规则缓存)去掉条件头,强制返回完整内容
|
|
58
|
+
const restoreConditionalHeaders = headers => {
|
|
59
|
+
const next = { ...headers };
|
|
60
|
+
const tags = String(next['if-none-match'] || '')
|
|
61
|
+
.split(',')
|
|
62
|
+
.map(tag => tag.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
if (tags.length && tags.every(tag => tag.endsWith(`${ETAG_SUFFIX}"`))) {
|
|
65
|
+
next['if-none-match'] = tags.map(tag => `${tag.slice(0, -(ETAG_SUFFIX.length + 1))}"`).join(', ');
|
|
66
|
+
} else {
|
|
67
|
+
delete next['if-none-match'];
|
|
68
|
+
delete next['if-modified-since'];
|
|
69
|
+
}
|
|
70
|
+
return next;
|
|
71
|
+
};
|
|
72
|
+
|
|
50
73
|
module.exports = {
|
|
51
74
|
rewriteLocation,
|
|
52
75
|
rewriteSetCookiePath,
|
|
53
76
|
shouldRewriteBody,
|
|
54
|
-
rewriteBody
|
|
77
|
+
rewriteBody,
|
|
78
|
+
tagEtag,
|
|
79
|
+
restoreConditionalHeaders
|
|
55
80
|
};
|
package/libs/utils/version.js
CHANGED
|
@@ -33,8 +33,10 @@ const runNpmInstall = (cwd, timeoutMs) =>
|
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/** install=false 时只解压校验,不装依赖(用于不常驻磁盘的版本上传) */
|
|
37
|
+
const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, maxZipEntries, npmInstallTimeoutMs, sqlPath, install = true, onProgress }) => {
|
|
37
38
|
await fs.emptyDir(artifactPath);
|
|
39
|
+
onProgress && onProgress('extract');
|
|
38
40
|
const extractDir = path.join(artifactPath, '_extract');
|
|
39
41
|
await extractZipSafe(zipBuffer, extractDir, { maxZipSize, maxZipEntries });
|
|
40
42
|
const packageRoot = await resolvePackageRoot(extractDir);
|
|
@@ -60,7 +62,10 @@ const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, max
|
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
const serverDir = path.join(artifactPath, 'server');
|
|
63
|
-
|
|
65
|
+
if (install) {
|
|
66
|
+
onProgress && onProgress('install');
|
|
67
|
+
await runNpmInstall(serverDir, npmInstallTimeoutMs);
|
|
68
|
+
}
|
|
64
69
|
|
|
65
70
|
const sqlDir = path.join(serverDir, sqlPath || 'sql');
|
|
66
71
|
const sqlFiles = await listSqlFiles(sqlDir);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { listSqlFiles } = require('./validatePackage');
|
|
4
|
+
|
|
5
|
+
/** 读取目录内的 .sql 为 [{ name, content, updatedAt }] */
|
|
6
|
+
const readSqlDir = async sqlDir => {
|
|
7
|
+
const files = [];
|
|
8
|
+
for (const name of await listSqlFiles(sqlDir)) {
|
|
9
|
+
const filePath = path.join(sqlDir, name);
|
|
10
|
+
const [content, stat] = await Promise.all([fs.readFile(filePath, 'utf8'), fs.stat(filePath)]);
|
|
11
|
+
files.push({ name, content, updatedAt: stat.mtime.toISOString() });
|
|
12
|
+
}
|
|
13
|
+
return files;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** 以 files 为准覆盖目录内的 .sql:删掉多余的,写入全部 */
|
|
17
|
+
const writeSqlDir = async (sqlDir, files) => {
|
|
18
|
+
await fs.ensureDir(sqlDir);
|
|
19
|
+
const keep = new Set(files.map(item => item.name));
|
|
20
|
+
for (const name of await listSqlFiles(sqlDir)) {
|
|
21
|
+
if (!keep.has(name)) {
|
|
22
|
+
await fs.remove(path.join(sqlDir, name));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const item of files) {
|
|
26
|
+
await fs.writeFile(path.join(sqlDir, item.name), item.content || '', 'utf8');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
readSqlDir,
|
|
32
|
+
writeSqlDir
|
|
33
|
+
};
|